方格取数(简单版)+小烈送菜(不知道哪来的题)-----------奇怪的dp增加了!
一、方格取数:
思路:emm...这咋写啊,好像是dp???可是你选了一个数后,第二次取数的时候不就不能取了,这不是有后效性了吗。。。
所以这道题的思路很清奇:两个人同时走。
这啥意思呢,简单来说,你的dp数组需要开四维,同时记录两个人的位置及状态,dp[i1][j1][i2][j2]。
既然我们一个人走两次会有后效性,那么我们可以认为是两个人同时在走,同时取数,这样就没有后效性了。
同时,转移的时候,就会有四种转移过程,第一个人可以从左或是从上过来,第二个人亦是。
转移方程:if(i1==i2&&j1==j2)dp[i1][j1][i2][j2]=max(dp[i1][j1][i2][j2],四种情况)+val[i1][j1];(这个意思是指两个人恰好走到一起去了,但是数只能取一次)
else dp[i1][j1][i2][j2]=max(dp[i1][j1][i2][j2],四种情况)+val[i1][j1]+val[i2][j2];
如下:
if(i1==i2&&j1==j2)dp[i1][j1][i2][j2]=max(dp[i1][j1-1][i2][j2-1],max(dp[i1-1][j1][i2][j2-1],max(dp[i1-1][j1][i2-1][j2],dp[i1][j1-1][i2-1][j2])))+val[i1][j1]; else dp[i1][j1][i2][j2]=max(dp[i1][j1-1][i2][j2-1],max(dp[i1-1][j1][i2][j2-1],max(dp[i1-1][j1][i2-1][j2],dp[i1][j1-1][i2-1][j2])))+val[i1][j1]+val[i2][j2];
啊啊啊啊太丑了太丑了
还有优化的方法:我们注意到,i1+j1==i2+j2==time(因为同时走的,速度也一样)
我们可以压缩成三维dp[i1][i2][time]遍历的时候跟上面大体一样,只要用time-i代替j就行了,注意time的循环范围!
代码全貌:
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; const int maxn=21; int dp[21][21][21][21],val[21][21]; int main(){ int n;scanf("%d",&n); while(1){ int x,y,z; scanf("%d%d%d",&x,&y,&z); if(x==0&&y==0&&z==0)break; val[x][y]=z; } for(int i1=1;i1<=n;i1++){ for(int j1=1;j1<=n;j1++){ for(int i2=1;i2<=n;i2++){ for(int j2=1;j2<=n;j2++){ if(i1==i2&&j1==j2){ dp[i1][j1][i2][j2]=max(dp[i1][j1-1][i2][j2-1],max(dp[i1-1][j1][i2][j2-1],max(dp[i1-1][j1][i2-1][j2],dp[i1][j1-1][i2-1][j2])))+val[i1][j1]; }else{ dp[i1][j1][i2][j2]=max(dp[i1][j1-1][i2][j2-1],max(dp[i1-1][j1][i2][j2-1],max(dp[i1-1][j1][i2-1][j2],dp[i1][j1-1][i2-1][j2])))+val[i1][j1]+val[i2][j2]; } } } } } printf("%d",dp[n][n][n][n]); return 0; }
二、小烈送菜
题目大意:一共有n位客人从1到n顺序排列,每位顾客i有满意值W[i],服务员小烈可以从1到n再从n走到1,在期间他到达i时可以选择送菜或不送,但是要求最后回到1时所有菜都送完,当送完菜给i后再送菜给j时,可以得到满意值Wi×Wj,问如何上菜总满意值最大(第一位被送菜的顾客不提供满意值)
思路分析:
这道题与方格取数很类似,小烈需要从1到n再从n到1走两遍,所以我们可以考虑同时维护两个小烈同时走,然后到n结束。
性质1:W[i]*W[j]==W[j]*W[i]
这意味着你从1到n走一遍和从n到1走一遍是等效的,你完全可以认为你从1到n中送菜的顾客1--->i--->j--->n满意度是W[1]*W[i]+W[i]*W[j]+W[j]*W[n],如果你反过来也一样:n--->j--->i--->1满意度是W[n]*W[j]+W[j]*W[i]+W[i]*W[1];显然两者等效。
(通过这条性质,我们可以把思路中的两个小烈都改为从1到n去走)
接下来我们需要定义dp数组:
因为需要定义的无后效性,所以我们保证:当小烈1位于i位置送菜,小烈2位于j位置送菜时,所有位置小于i、j的顾客都已经被送过菜了。
dp[i][j]表示小烈1位于i,小烈2位于j时的满意度最大值。
转移方程:
i可以从前一个状态k转移过来,j也是,但是这样去枚举k1,k2很麻烦(基本算不出来),所以我们考虑由i,j向后推。
我们可以知道,因为dp[i][j]表示i,j之前的所有点都被遍历过,所以i,j的下一个状态一定是i,i+1或i+1,j或j+1,j或i,j+1。这个根据i,j的大小而定,dp[i][j]的下一个状态一定是i,j两个小烈中靠前的那个的前面一步(否则就不满足i,j前的所有顾客都被遍历过了)。我们可以得到这样的方程:
if(i>j){ dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i]*a[i+1]); dp[i][i+1]=max(dp[i][i+1],dp[i][j]+a[j]*a[i+1]); }else{ dp[i][j+1]=max(dp[i][j+1],dp[i][j]+a[j]*a[j+1]); dp[j+1][j]=max(dp[j+1][j],dp[i][j]+a[i]*a[j+1]); }
这样写固然是没有问题啦,但是4行代码还是有点复杂,我们可以这样想:
如果保证dp[i][j]中i,j的大小关系,那么就可以把转移方程缩至两个。
我们不妨保证i>j。(在遍历的时候j从0到i-1即可)
那么方程变成:
dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i]*a[i+1]); dp[i][i+1]=max(dp[i][i+1],dp[i][j]+a[j]*a[i+1]);
这样已经很优秀了,可是我们看到第两个方程中i+1>i,这并不满足我们的前提条件i>j
这时候我们要用到性质2:
dp[i][j]=dp[j][i]这很显然吧,两个小烈本质上是一样的,谁在i,谁在j不重要,结果也一定相同。
所以我们可以改造一下dp[i][i+1],把它变成dp[i+1][i],这样就满足我们的i>j了。
最后的完成方程:
dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i]*a[i+1]); dp[i+1][i]=max(dp[i+1][i],dp[i][j]+a[j]*a[i+1]);
代码全貌:
#include<bits/stdc++.h> using namespace std; const int maxn=5510; int dp[maxn][maxn],a[maxn]; int main(){ int n; scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=1;i<=n;i++){ for(int j=0;j<i;j++){//注意这里j从0开始遍历,因为存在一种可能:一个小烈送完了1到i的所有客人菜 dp[i+1][j]=max(dp[i+1][j],dp[i][j]+a[i]*a[i+1]); dp[i+1][i]=max(dp[i+1][i],dp[i][j]+a[j]*a[i+1]); } } int Max=0; for(int i=0;i<n;i++){ Max=max(Max,dp[n][i]+a[i]*a[n]); } //我们假设第一个小烈已经到了终点,那么第二个小烈只需要从当前位置到n即可,因为i到n的所有点已经被第一个小烈走过了 //而实际上n这个点是要“被送两次的”,因为小烈到n再回去的时候a[n]会用两次。 printf("%d",Max); return 0; }