P7074 [CSP-J 2020] 方格取数
当时打眼一看没看见还能向上走,还在想为什么是个绿题……
进入正题。如果没有向上走的话,就是个经典简单的 dp。所以对于这个问题,我们还是先考虑 dp。
如果说我们按行从上往下转移肯定是有后效性的,不可行。但是我们发现,如果我们向右走,那我们是回不到左边的,这样就没有后效性了。所以我们从左往右转移,也就是从 \(j-1\) 列往第 \(j\) 列转移。
而对于这两列中的任意两个位置 \((k,j-1)\) 和 \((i,j)\),假设我们在 \((k,j-1)\) 这个位置向右走,那么由于不能走重复的格子,肯定是先向右,然后一直向上或向下的,不可能出现折返的情况。


我们设 \(dp_{i,j}\) 表示考虑到第 \(j\) 列,第 \(i\) 行的最大得分。这样我们就可以枚举上一列里向右走的那个位置了。朴素的转移方程如下:
这样是 \(O(n^4)\) 的,过不去啊。但是我们可以先求每行竖直方向的前缀和,优化掉累加的一个 \(n\),这样就是 \(O(n^3)\),还是过不去。
我们先把优化后的转移式子写一下:
因为我们的枚举顺序肯定是 \(j\) 在外层循环(首先保证是由 \(j-1\) 列转移来),所以枚举 \(i,k\) 时 \(j\) 是固定的,所以我们可以把有关 \(i\) 与 \(k\) 的项分开,也就是:
对于 \(k \le i\) 的情况,我们可以正序枚举 \(i\),并时刻记录一个最大值 \(mx\),其中 \(mx=\max\limits_{k=1}^{i}{dp_{k,j-1}-sum_{k-1,j}}\) 。
这样我们对于一个新加入的可以拿去更新的 \(i\) 直接 \(O(1)\) 更新,然后用 \(mx\) 转移 \(dp_{i,j}\) 即可。\(k > i\) 的情况同理。(不过要倒序枚举)
由于是取最大值,所以我们正着来一遍倒着来一遍,这样对于每个 \((i,j)\),它都被向下来的最优解和向上来的最优解更新过一遍,所以本做法是对的。
代码:
P7074
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x=0,f=1;char c=getchar();
while(c<48){
if(c=='-') f=-1;
c=getchar();
}
while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
return x*f;
}
inline void write(int x){
if(x<0) putchar('-'),x=-x;
if(x<10) putchar(x+'0');
else write(x/10),putchar(x%10+'0');
}
const int N=1314;
const int inf=1e16;
int n,m,a[N][N],dp[N][N],sum[N][N];
//sum[i][j]:对第j列做前缀和,当前做到第i行
signed main(){
n=read(),m=read();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
a[i][j]=read();
sum[i][j]=sum[i-1][j]+a[i][j];
}
}
//由于结果可能为负,所以初始化一定是赋极小值
for(int j=1;j<=n;j++){
for(int i=1;i<=m;i++){
dp[j][i]=-inf;
}
}
//对于第一列的数来说,显然只能从左上角往下走
for(int j=1;j<=n;j++){
dp[j][1]=sum[j][1];
}
for(int i=2;i<=m;i++){
int mx=-inf;
//mx:同题解
//计算k<=i时的最优解
for(int j=1;j<=n;j++){
//i是当前新加入的可更新mx的位置,更新mx
mx=max(mx,dp[j][i-1]-sum[j-1][i]);
dp[j][i]=max(dp[j][i],mx+sum[j][i]);
}
//计算k>i时的最优解
//同上
mx=-inf;
for(int j=n;j>=1;j--){
mx=max(mx,dp[j][i-1]+sum[j][i]);
dp[j][i]=max(dp[j][i],mx-sum[j-1][i]);
}
}
int ans=dp[n][m];
printf("%lld",ans);
return 0;
}

浙公网安备 33010602011771号