从递归到动态规划的代码转换之道

来源:互联网 发布:淘宝卖的衣服味道刺鼻 编辑:程序博客网 时间:2024/06/16 05:28

前言

昨天看了牛客网直播–左神的算法直播,讲了啊里巴巴的编程题,并由此展开,为我们讲了如何将暴力递归的代码一步一步转换成了动态规划的代码,简直妙哉!在这里我就利用自己有限的智商总结(抄袭)下如何将递归到动态规划的代码转换之道。

转换之道

直接上题讲解吧!

一排有N个位置,一个机器人在最开始停留在P位置,如果P==0位置,下一分钟机器人一定向右移动到1位置;如果P==N-1,下一分钟机器人一定向左移动到N-2位置。如果P在0N-1位置,下一分钟机器人一定会向左或者向右移动。求K分钟的时候,机器人到达T位置有多少种走法。 

这个题的递归过程应该可以很快的想到。核心就是K分钟的时候,机器人需到达T位置,那么K-1分钟时,那么机器人一定得到达T-1位置,或者T+1为位置。需要注意的是在T的初始值为0,即开始位置在0,那么K-1分钟时,机器人必定得在T+1位置,也就是1位置。同理当T的初始值为N-1,那么K-1分钟时,机器人必定得在T-1的位置,也就是N-2的位置。代码如下:

int f1(int N,int P,int K,int T){    if(N<2||P<0||K<1||T<0||P>=N||T>=N)//边界条件         return 0;     if(K==1){         return T==P?1:0;    }     if(T==0){//要到达T==0位置,机器人前一位置应该在1位置         return f1(N,P,K-1,T+1);    }    if(T==N-1){//要到达T==N-1位置,机器人前一位置应该在N-2位置         return f1(N,P,K-1,T-1);    }    //分别从前一位置的T-1位置或者T+1位置走。     return f1(N,P,K-1,T-1)+f1(N,P,K-1,T+1);}

那么怎么将上面的代码转换成相应的动态规划过程的代码。首先应该注意的是,在上面代码中,f1函数的四个参数,N,P的值是固定不变的,也就是机器人开始停留的位置,和一排的位置数目应该是固定,变化的是K跟T的值。可以这么来理解是,机器人只能在指定的一个范围来移动。而T的范围其实就是0~N-1,而K就是0~K-1啦。那么我们可以说f1函数得到的解空间其实就是在K*N的范围之内。那么问题就变成如何在解空间K*N的棋盘上填数了。

例如,我们现在假设K=5,N=5,P=2,T=2;那么解空间就是5*5的矩阵空间如下,且我们知道最开始(0,2)位置应该填上1,代表在开始我们到达 (0,2)有一种方法。而第一行的其他位置都应该为0。

这里写图片描述

那接下来我们就来理解下如何来填这张表。
1)由递归代码,我们知道当T==0时,我们想要得到该位置的值,也就(K,0)的解,那么我们需要从(K-1,T+1)的位置得到,也就是解空间(K,T)==(K-1,T+1).表现在解空间表就是矩阵的第一列的每一个解可以由上一行的右上方的解得到。如下图:

这里写图片描述

对于递归代码如下:

if(T==0){//要到达T==0位置,机器人前一位置应该在1位置        return f1(N,P,K-1,T+1);    }

2)当T==N-1时,我们想要得到该位置的值,也就(K,N-1)的解,那么我们需要从(K-1,T-1)的位置得到,也就是解空间(K,T)==(K-1,T-1),表现在解空间表就是矩阵的最后一列的每一个解可以由上一行的左上方的解得到。如下图:

这里写图片描述

对于递归代码如下:

if(T==N-1){//要到达T==N-1位置,机器人前一位置应该在N-2位置         return f1(N,P,K-1,T-1);    }

3)而当T为矩阵中间的位置时,其解空间的解可以由矩阵的上一行的左上方解和右上方的解得到。如下图:

这里写图片描述

对于递归代码如下:

return f1(N,P,K-1,T-1)+f1(N,P,K-1,T+1);

4) 那么接下来按照上面的步骤我们可以将解空间的值完全填满如下:

这里写图片描述

那么K=5,T=2应该为矩阵坐标(4,2)的值为6。


根据上面的分析填数规则,我们可以得到如下的动态递归的代码

int f2(int N,int P,int K,int T){//动态规划版本     if(N<2||P<0||K<1||T<0||P>=N||T>=N)//边界条件         return 0;    vector<vector<int> > dp(K,vector<int>(N,0));//解空间     dp[0][P]=1;//初始条件     for(int i=1;i<K;i++){//解空间行        dp[i][0]=dp[i-1][1];//边界0对应于T==0         dp[i][N-1]=dp[i-1][N-2];//边界N-1对应于T==N-1         for(int j=1;j<N-1;j++){//中间位置             dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];        }    }    return dp[K-1][T];}

同样我们可以进行代码空间复杂度的缩减,利用一个pre变量记录将之前遍历过的解,然后就可以只利用O(N)的空间。代码如下:

int f3(int N,int P,int K,int T){//动态规划版本空间优化     if(N<2||P<0||K<1||T<0||P>=N||T>=N)//边界条件         return 0;    vector<int> dp(N,0);    dp[P]=1;//初始化     int pre=0;    int tmp=0;    for(int i=1;i<K;i++){//解空间行        pre=dp[0];        dp[0]=dp[1];//解空间最左边的值等于上一行的右上方的值         for(int j=1;j<N-1;j++){//中间位置             tmp=dp[j];            dp[j]=pre+dp[j+1];            pre=tmp;        }        dp[N-1]=pre;    }    return dp[T];}

拓展题-纸牌博弈

有一个整型数组A,代表数值不同的纸牌排成一条线。玩家a和玩家b依次拿走每张纸牌,规定玩家a先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家a和玩家b都绝顶聪明,他们总会采用最优策略。请返回最后获胜者的分数。给定纸牌序列A及序列的大小n,请返回最后分数较高者得分数(相同则返回任意一个分数)。保证A中的元素均小于等于1000。且A的大小小于等于300。测试样例:[1,2,100,4],4返回:101

该题写出递归代码之后,也可以按照上面的转换之道写成相应的动态规划代码,代码如下:

/*有一个整型数组A,代表数值不同的纸牌排成一条线。玩家a和玩家b依次拿走每张纸牌,规定玩家a先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家a和玩家b都绝顶聪明,他们总会采用最优策略。请返回最后获胜者的分数。给定纸牌序列A及序列的大小n,请返回最后分数较高者得分数(相同则返回任意一个分数)。保证A中的元素均小于等于1000。且A的大小小于等于300。测试样例:[1,2,100,4],4返回:101*/#include <iostream>#include <vector>using namespace std;int s(vector<int> arr,int i,int j);int f(vector<int> arr,int i,int j);int s(vector<int> arr,int i,int j){//后拿牌函数     if(i==j){        return 0;//没牌了     }    /*        f(arr,i+1,j):表示如果先拿者拿了arr[i],即左边的牌,        那么后拿牌者只能在i+1..j的区间拿牌了。         f(arr,i,j-1):表示如果先拿者拿了arr[j],即右边的牌,        那么后拿牌者只能在i..j-1的区间拿牌了。        而且应该是后拿者所以只能得到两者最小的牌     */     return min(f(arr,i+1,j),f(arr,i,j-1));}int f(vector<int> arr,int i,int j){//先拿牌函数     if(i==j){        return arr[i];//如果只有一张牌,直接返回这张牌的分数     }     /*        arr[i]+s(arr,i+1,k):表示先拿了arr[i],即左边的牌,        那么后面拿牌只能在i+1..j的区间拿牌了。         arr[j]+s(arr,i,j-1):表示先拿了arr[j],即右边的牌,        那么后面拿牌只能在i..j-1的区间拿牌了。     */     return max(arr[i]+s(arr,i+1,j),arr[j]+s(arr,i,j-1));}int process1(vector<int> arr,int n){    if(arr.empty())        return 0;    //返回先拿牌的分数,和后拿牌分数的最大值     return max(f(arr,0,n-1),s(arr,0,n-1));}int process2(vector<int> arr,int n){    if(arr.empty())        return 0;    vector<vector<int> > f(n,vector<int>(n,0));//先拿牌解空间     vector<vector<int> > s(n,vector<int>(n,0));//后拿牌解空间     for(int j=0;j<n;j++){        f[j][j]=arr[j];//解空间对角线可以确定值         for(int i=j-1;i>=0;i--){            f[i][j]=max(arr[i]+s[i+1][j],arr[j]+s[i][j-1]);            s[i][j]=min(f[i+1][j],f[i][j-1]);        }     }    return max(f[0][n-1],s[0][n-1]);} int main(){    vector<int> arr={1,2,100,4};     cout<<process1(arr,4)<<endl;    cout<<process2(arr,4)<<endl;} 
阅读全文
0 0
原创粉丝点击