【问题描述】
设有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; }