1644.取数字问题

原题链接

外网进不去

题目大意

给定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

おすすめ

転載: blog.csdn.net/weixin_41247488/article/details/121572868