dp
动态规划一般都可以有递归改出来,大体的结构为:递归->记忆化搜索动态规划->严格表结构动态规划,记忆化搜索并不严格要求状态之间的依赖。而且记忆化搜索和严格表在某些问题上的时间复杂度是一样的。
记忆化搜索
严格表结构(还可以继续演化,更精简)。
如果一个函数,不管调用方是谁,函数返回的结果都一样,那么就可以改成一个无后效性(不管谁调,返回值都一样)的动态规划。
一个从暴力递归改成动态规划的栗子:
假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于 2。开始时机器人在其中的 M 位 置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到 1 位置, 那 么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到 N-1 位置。 规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是 1~N 中的一个)的方法有多少种。给 定四个参数 N、M、K、P,返回方法数。
递归版本应该是比较简单的。
int MaxMethod(int N,int M,int K,int P){
return recur(N,M,K,P);
}
//rest是剩余的步数,cur是当前的位置
//总的来说,有两个小限制,当走到1或N位置时,只能往回走
//M是目标位置
int recur(int N,int M,int rest,int cur){
//如果剩余步数为0
if(rest == 0){
return cur==M?1:0;
}
//1和N位置特别对待
if(cur == 1){
return(N,M,rest-1,2);
}
if(cur == N){
return(N,M,rest-1,N-1);
}
return recur(N,M,rest-1,cur-1)+recur(N,M,rest-1,cur+1);
}暴力递归为什么不好?因为重复计算的值太多了。那么我们可以想:搞个缓存存放已经计算的值,再次递归时,如果缓存命中,直接返回缓存中的值就行了。不命中再去计算。这就是所谓的记忆化搜索。
那么缓存开多大? 注意递归函数,前两个参数N、M都是固定的,跟递归没有关系。只有后两个参数与会该变递归函数的状态。所以缓存可以搞的二维表。(缓存表的大小、形式跟可以该变递归状态的参数个数有关)。
``` c++ "记忆化搜索" int MaxMethod(int N,int M,int K,int P){ //建立缓存表 //因为位置P的字面值为1~P,所以需要开辟P+1的长度,因为字面值P //同样剩余步数rest的字面值为0~K,所以需要开辟K+1的长度因为字面值K vector> cache(K+1,vector(P+1,-1)); //每个位置的元素都初始化为-1,代表没有计算过 return recur(N,M,K,P,cache); } int recur(int N,int M,int K,int P,vector>& cache){ //首先看当前位置是否已经计算过了 if(cache[K][P]!= -1){ return cache[K][P]; } //没有计算过,该递归递归,但是在返回结果前,需要将计算出的结果保存在cache中 if(P == 1){ cache[K][P]=recur(N,M,K-1,2); } if(P == N){ cache[K][P]=recur(N,M,K-1,N-1); } cache[K][P]=recur(N,M,K-1,P-1)+recur(N,M,K-1,P+1); return cache[K][P]; }
递归函数中只有两个变量与递归有关,所以需要开辟一个二维数组。 查看递归函数,index字面值范围为0~num.size()长度为nums.size()+1,aim字面值范围为0~aim,长度为aim+1。
可以看到,动态规划代码完全可以由递归形式改过来。 所以将递归改为动态规划可以有以下几个步骤:
确定哪些变量能够改变递归状态
确定上述变量的范围,从而分配dp数组大小(需要通过变量字面值确定)
确定dp数组中已知的答案(即base case)
通过递归形式,确定dp中一般位置的变量如何通过已知答案得到
确定dp中一般位置的位置的值是怎么来的
最后直接通过递归形式改
那么最后应该返回dp数组中的哪个值?
返回的值就是初始递归时的可变变量的值(即所想求的答案)。例如上述初始递归为(0,aim),那么最后就返回dp[0][aim]的值即可。
范围上的递归如何使用dp?
首先来解释一下什么是范围上的递归。一般情况下的递归题目都是从左不断尝试知道右边界或者相反。而范围上的递归则可能是从左边界尝试或是右边界尝试。不再是固定的方向。 下面以一个零和博弈的问题为例。
给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家 A 和 玩 家 B 都绝顶聪明。请返回最后获胜者的分数。
这是一个在范围上递归的题目。
上述是一个递归版本,下面讲述如何改为动态规划模型。首先这是一个范围模型。两个递归函数互相调用。所以dp表应该有两张。并且L、R的范围都为[0,arr.length-1],所以是一个正方形。以5X5为例。arr为[3,4,100,1,2]。初始模型如下:
先手表:
0
1
2
3
4
0
0
0
0
0
0
1
0
0
0
0
0
2
0
0
0
0
0
3
0
0
0
0
0
4
0
0
0
0
0
后手表:
0
1
2
3
4
0
0
0
0
0
0
1
0
0
0
0
0
2
0
0
0
0
0
3
0
0
0
0
0
4
0
0
0
0
0
首先使用base case填充已经知道的答案。一直,L不可能大于R,所以对角线下方都为0。填充后的dp表如下:
先手表:
0
1
2
3
4
0
3
0
0
0
0
1
0
4
0
0
0
2
0
0
100
0
0
3
0
0
0
1
0
4
0
0
0
0
2
后手表:
0
1
2
3
4
0
0
0
0
0
0
1
0
0
0
0
0
2
0
0
0
0
0
3
0
0
0
0
0
4
0
0
0
0
0
然后填充一般位置的dp表,由递归模型可以看出先手表中普通位置的元素是通过后手表对应位置的左边一个元素和下边一个元素相加而成。后手表的元素原理相同。 所以dp代码就可以很容易写出了。
递归转dp还是比较简单的,照着递归改就行了。主要是这道题是范围上的尝试。需要建立多张dp表。
多个可变参数如何使用dp?
多个可变的参数是指dp表的维度超过二维,达到三维甚至四维。下面是一道例题。
请同学们自行搜索或者想象一个象棋的棋盘,然后把整个棋盘放入第一象限,棋盘的最左下 角是(0,0)位置。那么整个棋盘就是横坐标上9条线、纵坐标上10条线的一个区域。给你三个 参数,x,y,k,返回如果“马”从(0,0)位置出发,必须走k步,最后落在(x,y)上的方法数 有多少种?
首先总结出递归的版本。马是走日字的。所以总结出在目标位置周围只走一步就可到达目标位置的点,然后继续,直到步数为0,并且回到(0,0)点。
这道题目中递归函数与三个变量有关,所以dp表应该是一个三维的,但是注意:三维表中的每一层都只与下一层有关,与当前层的任何点都无关。
感觉高维的dp和一维、二维的dp没啥区别啊。就是分析出当前层的值只与下一个层有关,并且立方体外的值都默认为0。然后照着递归就可以写出来了。所以最重要的还是得写出递归函数。
包含枚举的dp如何进行优化?
在动态规划中,如果在求一个位置上的数时,需要循环求某一列、或一行等等,反正是要用for循环才能求出一个位置的值怎么办?下面是一道使用斜率优化的题目。
有一个一维数组,数目中的每一个数表示一个硬币的面值,每种硬币的数量不限,给定一个目标金额,问有多少种方法组织出目标金额?
首先考虑递归版本:
查看递归模型,发现与递归相关的变量只有两个,所以dp表应该是一个二维数组,下面尝试写出dp版本。
如果当前arr[index]的值为1,则上述算法的最差复杂度为O(arr.lengthtargettarget),因为要从最右边迭代到最左边才能求出当前节点的值。那么如何进行优化呢?很简单,就是找规律。当前index行的值就是下一行同一列不断向左迭代累加的和。而这一迭代和却可以通过当前位置的左边的某一位置的和加上正下方的值而求得。这种优化的方法称作斜率优化法。没有为什么,就是简单的找规律。那么什么时候需要这种优化?在求dp表中某一位置的值时需要用循环才能求出,就可以考虑这种优化方法。
``` java "dp升级版" public static int maxWays(int[] arr,int target){ int[][] dp=new int[arr.length+1][target+1]; dp[arr.length][0]=1; for(int index=arr.length-1;index>=0;++index){ for(int rest=0;rest<=target;++rest){ int ways=0; for(int count=0;count*arr[index]<=rest;++count){ dp[index][rest]=dp[index+1][rest]; if(rest-arr[index]>=0){ dp[index][rest]+=dp[index][rest-arr[index]]; } } } } return dp[0][target]; }
```
我们有从左向右的模型,或范围内的尝试模型,那么如何评价尝试的模型?主要有一下两点标准:
单个可变参数的维度最好是一个整数,一定不要是别的类型,一定要弄成整数
可变参数的个数尽量少(这里指与递归相关的参数)
Last updated
Was this helpful?