zoukankan      html  css  js  c++  java
  • 暴力搜索算法,记忆搜索算法,动态规划算法


    以下这道题通过一步一步的分析优化可以看出暴力搜索方法,记忆搜索方法,动态规划方法的优化过程,往往写出暴力搜索方法是比较容易的,这样一步步的分析可以更好的理解动态规划方法。


    题目:

    给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。

    令arr={5,10,25,1},aim=1000。

    过程:

    用0张5元,让{10,25,1}去组成剩下的1000,方法记作res1

    用1张5元,让{10,25,1}去组成剩下的995,方法记作res2

    用2张5元,让{10,25,1}去组成剩下的990,方法记作res3
    ……………………

    用200张5元,让{10,25,1}去组成剩下的0,方法记作res201

    这样得到的每个结果,再用其他面值的货币去组成剩下的钱,利用递归,算出每个结果下剩下的钱有多少种组合,再把这201个结果的组合数相加得到最终的结果数。

    代码如下:

     public int coins1(int[] arr, int aim) {
            if (arr == null || arr.length == 0 || aim < 0) {
                return 0;
            }
            return process1(arr, 0, aim);
        }
    
        public int process1(int[] arr, int index, int aim) {
            int res=0;
            if (index == arr.length) {
                res = aim == 0 ? 1 : 0;
            } else {
                for (int i = 0; arr[index] * i <= aim; i++) {
                    res += process1(arr, index + 1, aim - arr[index] * i);
                }
            }
            return res;
        }

    以上就是递归得出结果。暴力搜索算法之所以暴力,就是需要重复之前的计算,之前已经计算过的,在后面的还要再计算一遍,比如使用0张5元和1张10元的情况下,那么后续process1方法的参数将是process1(arr,2,990),当用2张5元和0张10元的时候后续还是要计算process1(arr,2,990),因为钱数是固定的,用不同种的组合会有相同的条件下出现。这样会多出很多不必要的过程,非常影响效率。


    记忆搜索方法:


    之前我们进行计算的时候,process1中的参数,arr是不变的,只有index和aim在变,之所以暴力方法的时间长,是因为计算了很多重复的数据,既然要优化,减少复杂度,就要程序减少或者不进行重复的计算,只需要把之前计算的结果记录下来,这样后面再计算的时候,先进行判断,是否之前计算过,如果没有计算过,再进行递归计算,这样减少了很多的递归过程,可以提高很多效率。添加一个二维数组,这个二维数组记录的就是process2中变化的后面两个参数,把计算过的结果存在里面,后续的计算前,先进行判断。

    public int coins2(int[] arr, int aim) {
            if (arr == null || arr.length == 0 || aim < 0) {
                return 0;
            }
            int[][] map = new int[arr.length + 1][aim + 1];
            return process2(arr, 0, aim, map);
        }
    
        public int process2(int[] arr, int index, int aim, int[][] map) {
            int res = 0;
            if (index == arr.length) {
                res = aim == 0 ? 1 : 0;
            } else {
                int mapValue = 0;
                for (int i = 0; arr[index] * i <= aim; i++) {
                    mapValue = map[index + 1][aim - arr[index] * i];
                    if (mapValue != 0) {
                        res += mapValue == -1 ? 0 : mapValue;
                    } else {
                        res += process2(arr, index + 1, aim - arr[index] * i, map);
                    }
    
                }
            }
            map[index][aim] = res == 0 ? -1 : res;
            return res;
        }

    简单的说暴力搜索算法和记忆搜索算法,记忆搜索之所以比暴力快,因为记忆搜索算法把之前计算过的过程记下来了,有就直接用,没有就算

    记忆搜索方法就是动态规划的一种, 记忆搜索方法知识记录哪些过程计算过,哪些没有计算过,而动态规划方法是记录下来了每个计算的路径,怎么计算出这个的,后面的计算都会用到之前的计算过程。动态规划规定了计算顺序,而记忆化搜索方法不规定顺序,只关心结果

    我们用一个二维数组(矩阵)dp记录,dp[i][j]表示使用arr[0……i]组成总数为j的方法数,dp矩阵的求法和分析如下:

    对于矩阵dp第一列dp[i][0],表示的是组成钱的总数为0的组合数,只有一种,什么也不用,dp矩阵的第一列的值都设置为1

    对于矩阵dp第一行dp[0][i],表示的是用数组中下标为0的元素,可以组合成什么样的钱数,数组中第一个元素是5,很明显可以组成5,10,15……,5的倍数,这样把成倍数关系的位置也设置为1,也就是dp[0][arr[0] * j] = 1,j是满足大于0,arr[0]*j小于等于钱的总数

     对于剩下的任意dp[i][j],我们依次从左到右,从上到下计算,dp[i][j]的值可能来自下面:

    • 完全不使用当前货币arr[i]的情况下的最少张数,只是用arr[0……i-1]种货币,方法数为dp[i-1][j]的值

    • 只使用1张当前货币arr[i]的情况下的最少张数,剩下用arr[0……i-1]种货币,方法数为dp[i-1][j-1*arr[i]]

    • 只使用2张当前货币arr[i]的情况下的最少张数,剩下用arr[0……i-1]种货币,方法数为dp[i-1][j-2*arr[i]]

    • 只使用3张当前货币arr[i]的情况下的最少张数,剩下用arr[0……i-1]种货币,方法数为dp[i-1][j-3*arr[i]]

    • 只使用k张当前货币arr[i]的情况下的最少张数,剩下用arr[0……i-1]种货币,方法数为dp[i-1][j-k*arr[i]]
      ……
     public int coins3(int[] arr, int aim) {
            if (arr == null || arr.length == 0 || aim < 0) {
                return 0;
            }
            int[][] dp = new int[arr.length][aim+1];
            for (int i = 0; i < arr.length; i++) {
                dp[i][0] = 1;
            }
            for (int j = 1; arr[0] * j <= aim; j++) {
                dp[0][arr[0] * j] = 1;
            }
            int num = 0;
            for (int i = 1; i < arr.length; i++) {
                for (int j = 1; j <= aim; j++) {
                    num = 0;
                    for (int k = 0; j - arr[i] * k >= 0; k++) {
                        num += dp[i - 1][j - arr[i] * k];
                    }
                    dp[i][j] = num;
                }
            }
            return dp[arr.length-1][aim];
        }
    但是还有一个问题,实际运行就会发现,其实我们每次求得方法数还是把之前的运算结果列举出来,在实际的运行时间上也可以看出来实际上时间并没有减少

    我们接着优化其实我们发现要计算dp[i][j]的值就是dp[i][j-arr[i]]+dp[i-1][j],


    这样我们可以通过前面的结果直接可以得到最后的结果,没有必要再去挨个求

     public int coins3(int[] arr, int aim) {
            if (arr == null || arr.length == 0 || aim < 0) {
                return 0;
            }
            int[][] dp = new int[arr.length][aim+1];
            for (int i = 0; i < arr.length; i++) {
                dp[i][0] = 1;
            }
            for (int j = 1; arr[0] * j <= aim; j++) {
                dp[0][arr[0] * j] = 1;
            }
            int num = 0;
            for (int i = 1; i < arr.length; i++) {
                for (int j = 1; j <= aim; j++) {
                    dp[i][j] = dp[i - 1][j];
                    dp[i][j] += j - arr[i] >= 0 ? dp[i][j - arr[i]] : 0;
                }
            }
            return dp[arr.length-1][aim];
        }
    

    只是改变了一点,运行时间上已经可以达到1ms

    动态规划把状态的计算顺序规定了,所以较记忆搜索方法比动态规划时间复杂度大

    面对暴力搜索的优化:先用常见的递归方法写出暴力搜索方法,看暴力搜索方法中得到值的哪些可以记录下来,代表递归过程的参数,得到记忆搜索方法。

    动态规划自我感觉真的好难,其实最好想的就是记忆搜索方法,接着看看哪些可以一步步的优化


    以上根据左神的视频和书籍所写,加上了自己的一些总结,如有不对,欢迎指正!

    参考:程序员代码面试指南--左程云

  • 相关阅读:
    C++调试帮助
    C++中的前置(后置)++与--
    C++11 使用using定义类型别名
    C++11 尾置返回类型
    [BUUCTF]PWN——pwnable_hacknote
    [BUUCTF]PWN——ciscn_2019_es_7[详解]
    [BUUCTF]PWN——mrctf2020_easyoverflow
    [BUUCTF]PWN——wustctf2020_closed
    [BUUCTF]PWN——0ctf_2017_babyheap
    [BUUCTF]PWN——ciscn_2019_s_4
  • 原文地址:https://www.cnblogs.com/duzhentong/p/7816594.html
Copyright © 2011-2022 走看看