方格取数问题题解
问题描述:
在一个二维的数字矩阵a[x][y]中,从矩阵的左上角的数字开始,每次只能向右或向下走一个格,每经过一个方格,便取走方格上的数字,问走的那条路线上的所有元素的最大和是多少?
思路:这是一个可用动态规划解决的问题,首先建立动态方程,用f(i,j)表示走到第i行第j列时的所有元素的最大值。那么,f(i,j)=max( f(i-1,j) , f(i,j-1) )+a[i][j];当然,在i==0&&j!=0时,f(i,j)=f(i,j-1)+a[i][j];在i!=0&&j==0时,f(i,j)=f(i-1,j)+a[i][j];在i==0&&j==0时,f(i,j)=a[i][j]。
算法:用一个二维数组f[x][y]去存储状态f(i,j)的值,然后按照状态方程给f[x][y]赋值,最后去f数组中右下角的数字即可。
代码如下:
#include<cstdio>
#include<cstring>
int a[1000][1000];
int b[1000][1000];
int max(int x,int y){
return x>y?x:y;
}
int main(){
//memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
int n,m;
scanf("%d%d",&n,&m);//输入,二维矩阵的行数与列数
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
scanf("%d",&a[i][j]);//输入元素
}
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){//根据状态方程给数组b中的元素赋值
if(i==0&&j==0){b[i][j]=a[i][j];}
if(i==0&&j!=0){
b[i][j]=a[i][j]+max(0,b[i][j-1]);
}
if(i!=0&&j==0){
b[i][j]=max(b[i-1][j],0)+a[i][j];
}
if(i!=0&&j!=0){
b[i][j]=max(b[i-1][j],b[i][j-1])+a[i][j];
}
}
}
printf("%d\n",b[n-1][m-1]);//去最后的一个元素即可
return 0;
}
问题扩展1:(前后走两次求最大和)
假设要从左上角方格到右下角方格之间走两次,而且两次的路线可以走相同的方格,问这两条路线上的元素的最大值是多少?
思路:依然动归。考虑两条路线同时走,设res[i][j][ii][jj]是第一条路线达到(i, j)时、第二条路线到达(ii, jj)时,两条路线上元素的最大值。由于每个方格的走法只有“向下”、“向右”两种,因此,两条路线同时走的情况下res[i][j][ii][jj]的上一个状态一共有4种,分别是:res[i - 1][j][ii - 1][jj]、res[i - 1][j][ii][jj - 1]、res[i][j - 1][ii - 1][jj]、res[i][j - 1][ii][jj - 1],因此当前状态需要取上一步状态的最大值即可,于是状态转移方程是:res[i][j][ii][jj] = max(res[i - 1][j][ii - 1][jj], res[i - 1][j][ii][jj - 1], res[i][j - 1][ii - 1][jj],res[i][j - 1][ii][jj - 1]) + m[i][j] + m[ii][jj]。但是,由于当(i == ii && ii == jj)的时候,该方格的数值只能被加到一条路线上,于是需要多做一步减法:-m[i][j]。
代码如下:
#include<stdio.h>
#include<string.h>
int m[20][20];
int res[20][20][20][20];
int N;
#define max(a,b) \
(a > b ? a : b)
int main(int argc, char *argv[]){
scanf("%d", &N);
int a, b, c;
memset(m, 0x0, sizeof(m));
memset(res, 0x0, sizeof(res));
while(scanf("%d%d%d", &a, &b, &c)){
if(a == 0 && b == 0 && c == 0){
break;
}
m[a][b] = c;
}
int i, j, ii, jj;
for(i = 1; i <= N; i ++){
for(j = 1; j <= N; j ++){
for(ii = 1; ii <= N; ii ++){
for(jj = 1; jj <= N; jj ++){
res[i][j][ii][jj] = max(max(res[i - 1][j][ii - 1][jj], res[i - 1][j][ii][jj - 1]),
max(res[i][j - 1][ii - 1][jj], res[i][j - 1][ii][jj - 1]))
+ m[i][j] + m[ii][jj];
if(i == ii && j == jj){
res[i][j][ii][jj] -= m[i][j];
}
}
}
}
}
printf("%d\n", res[N][N][N][N]);
return 0;
}
问题拓展2:(记忆化搜索)
如果要求走两条互不相交的路线都到达右下角的位置,问这两条路线上的元素的最大值是多少?
思路:如果想要走两条路线,并且要去求两条路线的所有元素的最大值,而且两条路线互不相交,因此,只有当每条路线在各自的情况下都达到最优时,两条路线上的所有元素就是最大的。因此,当地一条路线遍历完,寻找到最大值路线时,我们需要将其路线上的每个元素都标记一下或者将其置为零,然后按相同的办法去规划另一条路线即可。(局部最优不代表全局最优,该问题暂时没有想到其他有效的解法,待续……)
算法:使用上面的数组b存储f(i,j)后,在b中倒叙搜索最大路径,并将路径上的每个点都置为-1。(如果想要输出路径,便可以将其置成其他的特征值,以便于搜索路径。)然后,再进行一次遍历,但是a[i][j]=-1的点不遍历,更新数组b的值。使用sum存储两次遍历的最大值之和即可。
代码如下:
#include<cstdio>
#include<cstring>
int a[1000][1000];
int b[1000][1000];
int max(int x,int y){
return x>y?x:y;
}
void find(int x,int y){
a[x][y]=-1;
if(x!=0||y!=0){
if(x==0&&y!=0){find(x,y-1);}
else if(x!=0&&y==0){find(x-1,y);}
else{
if(b[x-1][y]>b[x][y-1])find(x-1,y);
else find(x,y-1);
}
}
}
int main(){
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
int n;
scanf("%d",&n);
for(int i=0;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
scanf("%d",&a[x-1][y-1]);
}
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0){b[i][j]=a[i][j];/*a[i][j]=0;*/}
if(i==0&&j!=0){
b[i][j]=a[i][j]+max(0,b[i][j-1]);
//a[i][j]=0;
}
if(i!=0&&j==0){
b[i][j]=max(b[i-1][j],0)+a[i][j];
}
if(i!=0&&j!=0){
b[i][j]=max(b[i-1][j],b[i][j-1])+a[i][j];
}
}
}
//printf("%d\n",b[n-1][n-1]);
int sum=b[n-1][n-1];
// printf("%d\n",sum);
find(n-1,n-1);
memset(b,0,sizeof(b));
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(i==0&&j==0){b[i][j]=a[i][j];/*a[i][j]=0;*/}
if(i==n-1&&j==n-1)b[i][j]=max(b[i-1][j],b[i][j-1]);
if(a[i][j]!=-1){
if(i==0&&j!=0){
b[i][j]=a[i][j]+max(0,b[i][j-1]);
//a[i][j]=0;
}
if(i!=0&&j==0){
b[i][j]=max(b[i-1][j],0)+a[i][j];
}
if(i!=0&&j!=0){
b[i][j]=max(b[i-1][j],b[i][j-1])+a[i][j];
}
}
}
}
sum+=b[n-1][n-1];
printf("%d\n",sum);
return 0;
}