暴力搜索算法,记忆搜索算法,动态规划算法
以下这道题通过一步一步的分析优化可以看出暴力搜索方法,记忆搜索方法,动态规划方法的优化过程,往往写出暴力搜索方法是比较容易的,这样一步步的分析可以更好的理解动态规划方法。
题目:
给定数组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
动态规划把状态的计算顺序规定了,所以较记忆搜索方法比动态规划时间复杂度大
面对暴力搜索的优化:先用常见的递归方法写出暴力搜索方法,看暴力搜索方法中得到值的哪些可以记录下来,代表递归过程的参数,得到记忆搜索方法。
动态规划自我感觉真的好难,其实最好想的就是记忆搜索方法,接着看看哪些可以一步步的优化
以上根据左神的视频和书籍所写,加上了自己的一些总结,如有不对,欢迎指正!
参考:程序员代码面试指南--左程云