题目描述
给定数组arr,数组中所有的值都为正数,且不为空不重复。数组中每个数代表一种货币的面值,每种面值的货币可以使用任意张,然后,给一个aim,代表要换钱的面值,请问,用数组中的面值换钱,总共有多少种方式?
暴力递归思路
递归的第一步,将问题划分为更小的子问题,比如,数组arr[1,5,10]这三种面值的货币,aim=1000。首先,可以先取出第一种面值。假如,第一种1元的有0张,那么问题就划分为arr=[5,10],aim=1000;如果第一种1元的有1张,问题就划分为arr=[5,10],aim=999。依此类推,直到第一种货币的面值一元有1000张时,也就是超过aim,那么,第一种的所有结果都例举出来了。
因此,从上面分析中抽象出递归函数。首先,递归函数需要记录arr,和目前是第几种货币面值,也就是数组的索引,其次,还需要记录aim,当aim为0时,就找到一种换钱方法。
递归的出口是,数组遍历完最后一个,此时aim恰好为0,说明找到一个换钱方法,不为零,就不是,当然,不对的情况包含aim大于和小于的情况。
递归方法,就是设定一个res记录当前符合的换钱方式,遇到出口就返回1或0,否则,就继续向下递归。
递归代码
public static int process(int[] arr,int index,int aim){
int res=0;
if(index==arr.length){
res = aim==0 ? 1:0;
}else {
for (int i = 0; i*arr[index]<=aim; i++) {
res+=process(arr,index+1,aim-i*arr[index]);
}
}
return res;
}
记忆搜索法
暴力递归时间复杂度高的根本原因,就是存在大量的重复操作,而重复操作之所以大量发生,是因为在递归的过程中,很多重复计算的结果没有记录下来,才会导致重复计算。
因此,记忆搜索法其实也很简单,就是把每一步递归的结果放入一个map中,然后每次递归前,先检查map中有没有,如果已经有结果,直接取出来,就不需要重复计算了。
动态规划法
第一步,判断是否可行?本题中间递归结果是不依赖前面的路径的,因此可行。
第二步,画图,找出递归过程的变化量,index和aim,画图。
第三步,标记结果位置,此题的结果位置为index0,aim1000。
第四步,填入已知数据,已知数据为indexarr.length这一行,aim0的位置填返回值1,其他都是0。
第五步,根据已知结果,结合递归函数,我们已知了最后一行indexarr.length的结果,就可以根据递归函数,推出indexarr.length-1这一行的结果,然后依次填完表格,直到找到index0,aim1000位置的结果,就是返回的最终结果。
动态规划和记忆搜索对比
其实,动态规划可以理解为就是记忆搜索法 。
记忆搜索法不关心到达某一个递归过程的路径,只是单纯的记录递归过程的每个结果,避免重复计算。
动态规划是通过表格来表示每个递归过程之间的顺序,每个过程依赖前面已知的结果,其实也是在记录前面的递归结果,避免重复计算,只不过是也同时保留了递归顺序之间的过程。
两者都是空间换时间的优化思想,区别在于,动态规划记录计算顺序,而记忆搜索法只是简单避免重复计算,不记录计算顺序。
因此总结一下,什么动态规划?
其本质就是利用申请的额外空间来记录每一个暴力递归的结果,下次要用的时候可以直接使用,并且记录每种递归状态之间的计算顺序,依次计算。