动态规划(1)

来源:互联网 发布:获取百度统计的数据 编辑:程序博客网 时间:2024/06/07 09:11

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

思路:本题可以体现暴力搜索方法,记忆搜索方法,动态规划方法,状态继续化简后的动态规划方法,

(1)暴力搜索方法:

arr={5、10、25、1},aim=1000

1、用0张5元的货币,让[10,25,1]组成剩下的1000,最终方法数记为res1

2、用1张5元的货币,让[10,25,1]组成剩下的995,最终方法数记为res2

3、用2张5元的货币,让[10,25,1]组成剩下的990,最终方法数记为res3

........

201、用200张5元的货币,让[10,25,1]组成剩下的0,最终方法数记为res201

上面的每个res都有后续的递归过程,比如用剩下的三种货币组成一个钱数的过程我们认为是一个递归过程,总的方法数最终的结果是res1+res2+res3+....+res201

由上面的分析:定义递归函数:int p1(arr,index,aim),它的含义是如果用arr[index..N-1]这些面值的钱组成aim,返回总的方法数。

代码如下:

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);}}}
如果已经使用0元5元和1张10元的情况下,后续将求25,1这两种货币组成990的方法总数:p1(arr,2,990),即求2到990的方法总数,其中2:表示arr剩下的钱为 arr[2,3],即【25,1】

990:表示要找的剩余钱数。

当已经使用2张5元和0张10元的情况下,后续还是要求25,1这两种货币组成990的方法总数p1(arr,2,990),这种在重复计算在暴力递归的过程中大量发生,所以暴力递归方法的时间复杂度很高。

(2)记忆搜索法:

arr={5、10、25、1},aim=1000

暴力搜索方法的函数:

p1(arr,index,aim),其中arr始终不变,变化的只有index和aim,并且可以代表递归过程

用p(index,aim)来表示一个递归过程,重复计算之所以大量发生是因为每个递归过程的结果都没有记录下来,所以下次还要重复去求,所以可以实现准备一个哈希表记为map,

①、每计算完一个递归过程p(index,aim),都将结果放入map中,index和aim组成共同的KEY,返回结果为Value.下次进行递归之前现在map中查询递归过程是否已经计算过了,如果已经计算过了,就把值拿出来直接用即可,如果没计算过,则进行递归过程。

②、要进入一个递归过程p(index,aim)先以index,aim注册的KEY在map中查询是否已经存在value,如果存在,则直接取值,如果不存在,才进行递归计算。

实现伪代码:

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;}
coins2和coins1方法的区别是:实现准备好全局变量map记录已经计算过的递归过程的结果,防止下次重复计算。因为本题的递归过程课由
两个变量来表示,所以map是一张二维表,二维数组map[i][j]的结果代表p(i,j)的返回结果,在每一次进入下一次的递归之前,先查询是否已经计算过了,如果计算过就把结果从map中拿出来直接用,如果没计算过才进入递归过程,并且每个递归过程在计算完成后,都会将结果放入到map中,下次遇到相同的递归过程就可以防止重复计算了。其实记忆化搜索法可以认为是动态规划法。

③、动态规划方法

如果arr长度为N,生成行数为N,列数为aim+1的矩阵dp,dp[i][j]的含义是在使用arr[0...i]的货币的情况下,组成钱数j有多少种方法,dp的求法如下:

1、对于矩阵dp第一列的值表示组成钱数为0的方法数,很明显是一种,即不适用任何货币,所以dp第一列的值统一设置为1,

2、对于dp矩阵第一行的值,表示只能使用arr[0]这一种货币情况下,组成钱的方法数,比如arr[0]=5时,能组成钱的数只能是0,5,10,15等等,也就是说arr[0]的整数倍的位置,才能被arr[0]这种货币组成,那么其他钱数就通通不行了,所以就将相应位置设置为1,而其他位置设置为0,

3、除了第一行第一列,其他位置假设为位置[i][j],那么dp[i][j]的值是一下值的累加:

上面的方法数累加之后就是dp[i][j]的值,从左往右依次求出dp矩阵中每一行的值,然后再计算下一行的值,那么最终最右下角的值,即dp[N-1][aim]的值,就是最终结果,返回即可。


对于每一个位置i,j来说,求解dp[i][j]的计算过程需要枚举这个位置上上一排左边所有的值,



什么是动态规划:


2、动态规划规定每一种递归状态的计算顺序,从简单基本的状态出发,顺序的计算出所有的状态,最终获得结果的过程。

所以二者本质上是一致的,区别是动态规划严格规定计算顺序,而记忆搜索出现一种新的状态就计算。不关心状态依赖的路径,所以动态规划可以获得进一步优化的可能,


红色部分为枚举值累加的结果就是dp[i][j-arr[i]],所以dp[i][j]=dp[i][j-arr[i]]+dp[i-1][j],从而把枚举的过程彻底省略了,这种改进后的动态规划方法因为没有枚举的过程,所以把时间复杂度把O(N*aim的平方)降成了O(N*aim)。

虽然动态规划和记忆搜索法本质上是相同的,但是因为动态规划方法把计算的顺序规定了,从而让状态的进一步化简成为了可能。

面试中遇到的暴力递归题目可以优化成动态规划方法的大体过程:

1、实现暴力递归方法

2、在暴力搜索方法的函数中看看哪些参数可以代表递归过程。

3、找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现,把参数整体当做KEY,把递归过程的计算结果当成value,然后存入到map中。每个递归过程计算完成后都把结果放入map中,下次再碰到同样的状态计算时,就可以从map中直接取出来用了。

4、通过分析记忆化搜索的依赖路径,简单的可以直接得到的状态先计算,若是二维的话,相当于第一行和第一列,依赖于简单状态在整理出结果,那些复杂的状态在整理出正确的计算顺序之后,后计算,这种按照计算顺序下来的方式就是动态规划的方法,进而实现动态规划

5、根据记忆化搜索方法改出动态规划方法,进而看看是否能简化,如果能化简,还能实现时间复杂度更低的动态规划。

动态规划的经典问题:

求最长递增子序列的问题、0,1背包问题、硬币找零问题


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

给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。



原创粉丝点击