方格取数(简单版)+小烈送菜(不知道哪来的题)-----------奇怪的dp增加了!

一、方格取数:

设有N*N的方格图(N<=20),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字0。

某人从图的左上角的A(1,1) 点出发,可以向下行走,也可以向右走,直到到达右下角的B(n,n)点。在走过的路上(包括起点在内),他可以取走方格中的数(取走后的方格中将变为数字0)。此人从A点到B 点共走两次,试找出2条这样的路径,使得取得的数之和为最大。

 

思路: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;
}

        
原文地址:https://www.cnblogs.com/liu-yi-tong/p/13229055.html