暴力搜索算法,记忆搜索算法,动态规划算法


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


题目:

给定数组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

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

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

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


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

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

posted @ 2017-11-10 21:29  In_new  阅读(1937)  评论(0编辑  收藏  举报