方格取数 (多进程DP)

时间:2022-02-17 16:40:06

【问题描述】

设有N*N的方格图(N<=10,我们将其中的某些方格中填入正整数,而其他的方格中则放入数字0。如下图所示(见样例):

某人从图的左上角的A 点出发,可以向下行走,也可以向右走,直到到达右下角的B点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。

此人从A点到B 点共走两次,试找出2条这样的路径,使得取得的数之和为最大。

【输入文件】

输入的第一行为一个整数N(表示N*N的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的0表示输入结束。

【输出文件】

只需输出一个整数,表示2条路径上取得的最大的和。

【输入样例】

8

2  3  13

2  6  6

3  5  7

4  4  14

5  2  21

5  6  4

6  3  15

7  2  14

0  0  0

【输出样例】

67

【问题分析】

这是一道很经典的多进程动态规划题,如果只取一次,它的模型就是我们前面讲的街道问题了,很简单就可以实现。现在要求取两次,怎么办呢?

一个自然的想法是:将前面的取过的数全赋成0,然后在取一次,感觉挺对的,样例也过了。

但这样做是错的,第一次取的显然是最大值,但第二次取的未必次大,所以也许两条非最大的路,也许比一条最大一条小一点的路更优。

看个例子:

0 0 2 0 30                     0 0 2 0 3 0

0 0 2 0 0 0                     00 2 0 0 0

0 0 2 0 20                     0 0 2 0 2 0

0 0 0 0 20                     0 0 0 0 2 0

0 0 0 0 20                     0 0 0 0 2 0

0 0 2 0 20                     0 0 2 0 2 0

图1                               图2

如上图,图1是按找上诉思路求得的解。图中红色路线是第一求得的最大值,显然图1红色和紫色两条路径不如图2蓝色和绿色两条路径大。

既然这样做不行,我们还得回到动态规划的本质来看代问题,我们在想想这个问题的状态,对于走一次,走到矩阵的任意一个位置就是一个状态,而要是走两次,显然走到矩阵的某个位置只是一个状态的一部分,不能完整的描述整个状态。那另一部分显然就是第二次走到的位置了。如果我们把这两部分合起来就是一个完整的状态了。

于是,设计一个状态opt[i1,j1,i2,j2]表示两条路分别走到(i1,j1)点和(i2,j2)点时取到的最大值。显然决策有4中(乘法原理一个点两种*另一个点的两中)

即(上,上)(上,左)(左,上)(左,左)上和左表示从哪个方向走到该点,当然要注意走到同行,同列,同点时的情况(因为要求路径不重复)。

【状态】f[i][j][k][l]表示两人分别到(i,j)、(k,l)所取过的数的和.G[i][j]表示方格里的数.

【方程】f[i][j][k][l] = max{f[i-1][j][k-1][l], f[i-1][j][k][l-1], f[i][j-1][k-1][l], f[i][j-1][k][l-1]}+G[i][j]+(i==k&&j==l ? 0 : G[k][l])

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int a[11][11];
int dp[11][11][11][11];
const int INF = 999999999;

int operDp(int n)
{
    int i1, j1, i2, j2;
    memset(dp,0,sizeof(dp));
    for(i1 = 1; i1 <= n; i1++)
    for(j1 = 1; j1 <= n; j1++)
    for(i2 = 1; i2 <= n; i2++)
    for(j2 = 1; j2 <= n; j2++)
    {
        int tmp = -INF;
        tmp = max(tmp, dp[i1-1][j1][i2-1][j2]);
        tmp = max(tmp, dp[i1-1][j1][i2][j2-1]);
        tmp = max(tmp, dp[i1][j1-1][i2-1][j2]);
        tmp = max(tmp, dp[i1][j1-1][i2][j2-1]);
        if(i1 == i2 && j1 == j2)
            dp[i1][j1][i2][j2] = tmp + a[i1][j1];
        else
            dp[i1][j1][i2][j2] = tmp + a[i1][j1] + a[i2][j2];
    }
    return dp[n][n][n][n];
}

int main()
{
    int n, r, c, v;
    while(scanf("%d",&n) != EOF)
    {
        memset(a,0,sizeof(a));
        while(true)
        {
            scanf("%d%d%d",&r,&c,&v);
            if(!r && !c && !v) break;
            a[r][c] = v;
        }
        printf("%d\n",operDp(n));
    }
    return 0;
}


由于本题的数据规模很小,所以(n^4)的方法也能解决。但是的规模增大的时候我们就必须对算法进行优化了。

很明显dp[i1][j1][i2][j2]的四维状态描述了所有可能的走法,当然我们也可以改变状态的表示法,减少状态的维数。

f[k][i][j] = max { f[k-1][i][j], f[k-1][i-1][j-1], f[k-1][i-1][j],f[k-1][I][j-1] } + (i==j ? a[k-i][i] : a[k-i+1][i]+a[k-j+1][j])

f[k][i][j]表示走了k步,第一条路向右走i步,第二条路向右走j步。

每条路的每个位置都可以从它的上方或左方得到,所以max{}里会有四个状态。还有
如果两条路走到了同一位置,那么该位置的数只加一次.

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

int a[11][11];
int dp[21][11][11];
const int INF = 999999999;

int operDp(int n)
{
    int i, j, k;
    memset(dp,0,sizeof(dp));
    for(k = 1; k <= 2 * n; k++)
    for(i = 1; i <= k; i++)
    for(j = 1; j <= k; j++)
    {
        int tmp = -INF;
        tmp = max(tmp, dp[k-1][i-1][j-1]);
        tmp = max(tmp, dp[k-1][i-1][j]);
        tmp = max(tmp, dp[k-1][i][j-1]);
        tmp = max(tmp, dp[k-1][i][j]);
        if(i == j) dp[k][i][j] = tmp + a[k-i+1][i];
        else dp[k][i][j] = tmp + a[k-i+1][i] + a[k-j+1][j];
    }
    return dp[2*n][n][n];
}

int main()
{
    int n, r, c, v;
    while(scanf("%d",&n) != EOF)
    {
        memset(a,0,sizeof(a));
        while(true)
        {
            scanf("%d%d%d",&r,&c,&v);
            if(!r && !c && !v) break;
            a[r][c] = v;
        }
        printf("%d\n",operDp(n));
    }
    return 0;
}