原题链接
题目大意
给定M*N的矩阵,其中的每个元素都是-10到10之间的整数。从左上角往右下角走,每一步只能向右或向下,并且不能走出矩阵的范围。你所经过的方格里面的数字都必须被选取,请找出一条最合适的道路,使得在路上被选取的数字之和是尽可能小的正整数。
解题思路
这个题目,感觉可以使用数塔问题的动态规划方法,但是当你仔细研究之后会发现,题目要求是: 使 得 在 路 上 被 选 取 的 数 字 之 和 是 尽 可 能 小 的 正 整 数 使得在路上被选取的数字之和是尽可能小的正整数 使得在路上被选取的数字之和是尽可能小的正整数 如果使用普通的动态规划,因为答案只有一个,所以只能是最小的数字,不一定是正数,于是,做法被证伪了。那么如何来做呢?做法有两个:
方法1:动态规划(判定性问题)
其实,动态规划的方法之所以不行,是因为得到的一定是最小的答案,只有一个。那如果是再加上一个特判,必须要是正整数才可以更新数组可以吗?其实也不可以,因为中间如果都得是正整数,那就不一定是最小了。这怎么办?就只能在一个位置上记录多个数字。
有了这个想法,不难推导出这样的转移方程,如果当前位置的坐标是
(
i
,
j
)
(i,j)
(i,j)的话,
f
i
,
j
,
k
f_{i,j,k}
fi,j,k如果为
t
r
u
e
true
true就表示到达这个位置所走过的路线上数字的和有可能为
k
k
k,否则不可能,而当前位置上的值为
t
r
u
e
true
true的
k
k
k就是左边外位置为
t
r
u
e
true
true的所有
k
k
k再加上当前位置所对应的数,和上面外位置为
t
r
u
e
true
true的所有
k
k
k再加上当前位置所对应的数(由于有可能出现负数,我们就需要将所有的数字向右移动一定的位置,使最小的负数也可以变成非负数,由题可知,这个数应该是
10
×
10
×
10
10\times 10\times 10
10×10×10 但是,为了保险起见,最好使用
1100
1100
1100)。这么做之后答案就是右下角从
1100
1100
1100 开始最小的的
k
k
k 再减去
1100
1100
1100 。
代码实现
#include<iostream>
#include<cmath>
using namespace std;
const int M=1100;
int n,m,a[20][20];
bool f[20][20][2200];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
}
}
f[1][1][a[1][1]+M]=true;//边界条件,初始的位置,加上M防止出现负数
for(int i=2;i<=n;i++){//初始化,第一列
for(int j=1;j<=M*2;j++)//枚举可能的数字
if(f[i-1][1][j])//只能从上面过来,判断所枚举的数字
f[i][1][j+a[i][1]]=true;//改变当前位置
}
for(int i=2;i<=m;i++){//初始化,第一行
for(int j=1;j<=M*2;j++)//枚举可能的数字
if(f[1][i-1][j])//只能从左边过来,判断所枚举的数字
f[1][i][j+a[1][i]]=true;//改变当前位置
}
for(int i=2;i<=n;i++){//枚举行,从2开始
for(int j=2;j<=m;j++){//枚举列,从2开始
for(int k=1;k<=M*2;k++)//枚举可能的数字
if(f[i-1][j][k])//从上面过来,判断所枚举的数字
f[i][j][k+a[i][j]]=true;//改变当前位置
for(int k=1;k<=M*2;k++)//枚举可能的数字
if(f[i][j-1][k])//从左边过来,判断所枚举的数字
f[i][j][k+a[i][j]]=true;//改变当前位置
}
}
for(int i=M+1;i<=M*2;i++)//因为所有位置都加上了M,所以其实M+1才是真正的1
if(f[n][m][i]){//找到了答案
cout<<i-M;//还原答案
return 0;
}
return 0;
}
方法2:递归算法,记忆化优化
如果想要从右下角推到左上角,那么使用递归就最合适了。我们可以先写一个记忆化搜索,有三个参数,分别表示第几行,第几列和当前数字是几,每一次递归向上或者左递归,一直知道左上角。记忆化的方法和动态规划很像,也是使用 f i , j , k f_{i,j,k} fi,j,k表示在坐标为 ( i , j ) (i,j) (i,j)时,从左上角走到当前位置所经过位置对应数的和是为 k k k 否可行。我们可以一直枚举答案,再使用递归来判断是否可行,也就是左上角的数字是 1010 1010 1010 (防止出现负数所加上的数,也就是真正的0点)。
代码实现
#include<iostream>
#include<cmath>
using namespace std;
const int M=1100;
bool f[20][20][2200];
int n,m,a[20][20];
void dfs(int x,int y,int d){
f[x][y][d+M]=1;//记忆化+判重
if(f[1][1][M]) return;//终止条件之一,达到终点(另外一个是无法扩展)
if(x>1&&!f[x-1][y][d-a[x-1][y]+M]) dfs(x-1,y,d-a[x-1][y]);//记忆化+判重
if(y>1&&!f[x][y-1][d-a[x][y-1]+M]) dfs(x,y-1,d-a[x][y-1]);//记忆化+判重
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=M;i++){//枚举答案
dfs(n,m,i-a[n][m]);//运行递归
if(f[1][1][M]){//检查答案
cout<<i;
return 0;
}
}
cout<<-1;
}
样例
输入
2 2
0 2
1 0
输出
1