题目描述
设有 n×m 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。
输入格式
第一行有两个整数 n,m
接下来 n 行每行 m 个整数,依次代表每个方格中的整数。
输出格式
一个整数,表示小熊能取到的整数之和的最大值。
输入输出样例
3 4 1 -1 3 2 2 -1 4 -1 -2 2 -3 -1
9
2 5 -1 -1 -3 -2 -7 -2 -1 -4 -1 -2
-10
说明/提示
样例 1 解释
样例 2 解释
数据规模与约定
- 对于 20% 的数据,n,m≤5
- 对于 40% 的数据,n,m≤50
- 对于 70% 的数据,n,m≤300
- 对于 100% 的数据,1≤n,m≤10^3
- 方格中整数的绝对值不超过 10^4。
这个题看着很简单,做起来好难啊qwq
一开始以为是dp,结果就是dp,但是还是不会做(╥╯^╰╥)
好啦,既然不会做,那么就一起来学习一下正解的思路吧!!
首先,在开始之前还是先来讲一讲我自己的思路(当然这个是错的)
我以为这道题就只是一个简单的dp而已,与其他dp不同的地方只是在于这个题目要求中,上下都可以走,也就是说小熊可以往复回环走,所以我们脑海中浮现的第一想法不就是:害,加一个bool判断一下这个格子有没有走过不就行了吗?还用得着其他东西吗?于是我这么想的,也这么做的,最后也这么抱灵的~~
那么问题来了,上面的方法为什么是错的呢?
我们这样来想,我们如果在小熊走过的路上进行标记,那么结果有两证:
1.每经过一个点标记一次小熊的路径,但是很快我们发现这样不行,如果这样的话我们是按照一定的顺序从上到下遍历格子来求每个格子的最优解的,那么如果这样标记,我们最后的结果就只能是把所有的格子都标记下来,那么如果之后我们更新的时候发现还存在更优的解,那么我们就不能再次更新了,因为这个点在之前已经被标记了,所以这种标记方法不可取
2.我们给这个点设置三个变量,分别表示其是否是从上下左三个方向转移过来的,那么如果是这样的话,其实根本处境还是没有什么改变,如果存在一条更优的路径,那么因为之前我们已经把这个点进行了标记,这个点就不可以再走了,所以那一条更优路径就会被我们舍弃。所以最后不一定可以找到最优解。
至此,我发现凭借我的可怜巴巴的一点点的智商,可能没有办法利用标记法来完成这道题,所以我愉快(并不)的放弃了这道题:)
正解来啦||ヽ(* ̄▽ ̄*)ノミ|Ю~~~
让我来为你们详细解释一下正解~~
首先我们来观察这个题与其他题的不同点在哪里。其他的dp题只能走上右或者下右,但是这个题却可以走上下右,所以凭借单纯的dp肯定是无法解决这个问题的。
然后,对于每一列,由于题目规定我们“不能重复经过已经走过的方格”,所以我们在每一列上只能向上走或者向下走,而不能上下来回走,这样就重复了。而只能走右不能走左,则表示对于每一列来说,当前一列的最大值一定是从左边那一列推出来的,所以我们可以按照一列来进行划分,把每一列当成一个整体来求解。
我们再来整理一下上面思路的来源:
1.只能向右走—>可以按照列来划分,从左到右对每列进行求解
2.不能重复经过已经走过的方格—>每个方格只能向上或者向下走—>设置两个变量来保存从上面向下走的最优解和从下面向上走得最优解
这样这个题的思路就很明白了,我们设置两个数组 up[i][j] 和 down[i][j] ,分别表示从下面走上来到 i 行 j 列的最大值和从上面走下来到 i 行 j 列的最大值。
当然最大值也是有可能从左边来的,那么我们还需不需要再开设一个变量 left[i][j] 表示从左走到右的最大值呢?
答案是不需要的,因为从上文分析中我们可以得出结论,我们只能从左走到右,方向是唯一确定的,所以我们只需要用我们的 f[i][j] 总变量来表示第 i 行 j 列的点的最大值,而不需要再设置一个变量表示从左走到右的最大值(当然你也可以理解为这两个变量所表达的意思其实是相同的)。
那么状态转移方程就可以写出来了(a[i]][j] 表示 i 行 j 列的元素值)
up[i][j]=max(up[i+1][j],f[i][j-1])+a[i][j];
down[i][j]=max(down[i-1][j],f[i][j-1])+a[i][j];
f[i][j]=max(up[i][j],down[i][j]);
我们发现如果这样设置状态,这道题的确可以AC,但是我么在前面的分析中其实还有一点没有用到“可以把列当成一个整体来处理”。
考虑优化:
其实对于当前位置的元素,它的 down 和 up 只由上一列的元素的最大值转移而来所以我们完全可以进行压维操作。
设置变量 up ,down ,f 分别为 up[i] , down[i] , f[i] ,分别表示这一行从下面走上来的最大值,这一行从上面走下来的最大值,这一行的最大值(注意,因为我们可以把列当做一个整体,所以可以把一整列进行压缩,但是行是不可以压缩的,所以最后设置的一维变脸都是用来保存行最优解的)。
那么状态转移方程就变成了:(不行,我觉得下面的方程需要我来解释一下了,毕竟当时我就是卡在这里出不来的qwq)
up[i]=max(f[i],up[i+1])+a[i][j]; //这一行从下到上的最大值要么是从下一行的up最大值转移来的,要么是从这一行的最大值( f[i] )转移来的.为什么还会从 f 转移过来呢?因为我们上面就已经说过了,我们在这里可以把 f 看做 left 变量,{因为 f 是从左到右更新的},所以当前最大值也可以从 f[i] 转移过来。
down[i]=max(f[i],down[i-1])+a[i][j]; //同理,这一行从上到下的最大值要么是从上一行down的最大值转移过来的,要么是从这一行的最大值 f[i] 转移过来的。
f[i]=max(up[i],down[i]); //因为我们每更新完一次down 和 up ,这一行的最大值就有可能发生变化,所以 f 也应当更新,但是问题有来了,为什么 f 不需要和自己进行比较一下呢?有没有可能当前的 down 和 up 都没有 f 自己的值大呢?答案是不可能的,因为down 和 up 就是从 f 转移过来的,在转移结束之前他们已经和 f 进行过比较,也就是说他们是和f 进行比较之后,又加上了当前方格中的元素,才转移过来的,所以up 和down 的值一定比 f 大,所以我们只需要比较up 和down 的大小即可。
好了,啰里啰嗦半天终于把所有事情都给解释清楚了,但是代码中还存在一个令人迷惑的地方
/*for(int j=2;j<m;j++)*/{ memset(down,0,sizeof(down)); memset(up,0,sizeof(up)); down[1]=f[1]+a[1][j];up[n]=f[n]+a[n][j]; for(int i=2;i<=n;i++) down[i]=max(f[i],down[i-1])+a[i][j]; for(int i=n-1;i>=1;i--) up[i]=max(f[i],up[i+1])+a[i][j]; for(int i=1;i<=n;i++) f[i]=max(down[i],up[i]); }
看见变成绿色的部分了吗?为什么对于列的循环我们只对2到m-1进行更新,而不对1和m进行更新呢?我们来自习思考一下,其实对于第一列和最后一列来说,他们在我们心中都是特殊列。什么叫特殊列呢?对于第一列,我们发现它不能从左边更新到右边,对于最后一列,我们发现它不能从下面更新到上面,所以我们需要对它们进行单独处理,因为这样干讲可能对于理解效果不太好,所以我们来借助AC代码理解一下:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int n,m; int a[1001][1001]; long long up[1005],down[1005],f[1005]; inline int read(){ int f=1,x=0;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} return f*x; } int main(){ n=read(),m=read(); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) a[i][j]=read(); f[1]=a[1][1]; for(int i=2;i<=n;i++) f[i]=f[i-1]+a[i][1];//设置f的初始值,其实也相当于对第一列进行特殊处理 //但是为了增强代码的客观性,所以我们在这里先同样对第一列特殊化处理,只更新第二列的值 for(int j=2;j<m;j++){//循环更新列 memset(down,0,sizeof(down)); memset(up,0,sizeof(up)); down[1]=f[1]+a[1][j];up[n]=f[n]+a[n][j]; for(int i=2;i<=n;i++) down[i]=max(f[i],down[i-1])+a[i][j]; for(int i=n-1;i>=1;i--) up[i]=max(f[i],up[i+1])+a[i][j]; for(int i=1;i<=n;i++) f[i]=max(down[i],up[i]);//我们在这里对于第一列,最后一列的值进行特殊处理, //因为上面我们已经把down[1] up[n]更新完了,所以对于第一列来说它的值就是从上到下遍历过后的值,也就是说对于第一列 //而言,它只能从上到下走(自己思考一下?)而不能有其他路径,所以我们可以凭借down 和up 对f 进行更新 } f[1]+=a[1][m];//处理一下细节 for(int i=2;i<=n;i++) f[i]=max(f[i],f[i-1])+a[i][m];//然后问题又来了,最后一列我们还没有处理呢,还记得f的定义吗? //走到当前格子的最大值,而最后一列只有可能从上面或者从左面转移过来,所以取从上面走下来,从左面走过来的最大值,加上 //当前格中元素的值就可以得到当前格子中的最大值 printf("%lld",f[n]);//输出结果end~~ :) return 0; }
---------end----------