题解 P7074 【方格取数(民间数据)】

WanderingTrader

2020-11-15 22:38:50

Solution

好不容易有了一次给CSP写题解的机会,当然不能放过了。(滑稽) 比赛的时候先写了个 $O(n^2m)$ 的朴素dp,然后用10min优化到了 $O(nm).$ ### 题目分析 我们先把状态定义出来: 令 $f(i,j)$ 为截止到第 $(i,j)$ 格时取到的整数之和最大值。 然而,小熊既可以往上走又可以往下走。如果直接按照状态转移的顺序进行连边,最终所得的图将不是一个DAG,直接求最长路会TLE或者WA. 突破口在于:虽然在上下方向可以随意走,但在左右方向只能向右。最终走到终点的时候,上下方向可能已经走了许多次,但左右方向一定只走了 $m-1$ 次。 于是我们把状态稍作修改: 令 $f(i,j)$ 为截止到第 $(i,j)$ 格,**而且走到此格后立刻向右走一步** 时取到的整数之和最大值。 那么转移路径就会由本来的这样: ![](https://s3.ax1x.com/2020/11/15/DFcsgg.png) 变成这样(为了画面整洁只画出部分): ![](https://s3.ax1x.com/2020/11/15/DFhOVH.png) 很显然,这是一个DAG,可以直接做。 那么状态转移方程即为: $f(i,j)=\max\{f(k,j-1)+s(i,k,j)|k\in[1,n]\}$ 其中 $s(i,j,k)=\begin{cases}\sum\limits_{x=i}^{j-i+1}a(x,k)&(i<j)\\a(i,k)&(i=j)\\\sum\limits_{x=j}^{i-j+1}a(x,k)&(i>j)\end{cases}$,也就是第 $k$ 列上从 $i$ 到 $j$ 的连续和。 那么如果用前缀和优化,这个方程直接算用 $O(n^2m)$ 就可以了。 ```cpp void NsquareM() { for(int j=2;j<=m;++j) { for(int i=1;i<=n;++i) { f[i][j]=-inf; for(int k=1;k<=i;++k) f[i][j]=max(f[i][j],f[k][j-1]+s[i][j]-s[k-1][j]); for(int k=i+1;k<=n;++k) f[i][j]=max(f[i][j],f[k][j-1]+s[k][j]-s[i-1][j]); } } printf("%lld\n",f[n][m]); } ``` 注意计算之前别忘了先设定成 $-\inf.$ 那么如何优化呢?我们从代码入手: ```cpp f[k][j-1]+s[i][j]-s[k-1][j] f[k][j-1]+s[k][j]-s[i-1][j] ``` 我们发现代码中一个很重要的特点:$i$ 和 $k$ 从不同时出现。那么我们可以分别把含 $i$ 的式子和含 $k$ 的式子整理出来: ``` f[k][j-1]+s[i][j]-s[k-1][j] --- s[i][j] + (f[k][j-1]-s[k-1][j]) f[k][j-1]+s[k][j]-s[i-1][j] --- -s[i-1][j] + (f[k][j-1]+s[k][j]) ``` 那么当 $j$ 确定时,括号里式子的最大值应该是定值,不随 $i$ 的变化而变化,因此可以预处理。 ```cpp void NM() { rMAX[n+1]=-inf; for(int j=2;j<=m;++j) { LL MAX=-inf; for(int i=n;i;--i) rMAX[i]=max(rMAX[i+1],f[i][j-1]+s[i][j]); for(int i=1;i<=n;++i) { f[i][j]=-inf; MAX=max(MAX,f[i][j-1]-s[i-1][j]); f[i][j]=max(MAX+s[i][j],rMAX[i+1]-s[i-1][j]); } } printf("%lld\n",f[n][m]); } ``` (话说直接用时间复杂度做函数名的应该就我一个了吧) 这里MAX由于是顺推,所以直接一个变量 而 rMAX由于是逆推需要数组。 两重循环内部就这么变成了 $O(1)$ 。 比赛时担心优化写挂,于是主函数这样写了(freopen已注释): ```cpp #include <bits/stdc++.h> using namespace std; typedef long long LL; const int maxn=1e3+5; const LL inf=1e12; int n,m; LL a[maxn][maxn],f[maxn][maxn],s[maxn][maxn],rMAX[maxn]; int main() { //freopen("number.in","r",stdin); //freopen("number.out","w",stdout); scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) { scanf("%lld",&a[i][j]); s[i][j]=s[i-1][j]+a[i][j]; } for(int i=1;i<=n;++i) f[i][1]=s[i][1]; if(n*n*m<=1e7) NsquareM(); else NM(); return 0; } ``` 根据数据大小用不同的算法,这也是比赛时的一个小技巧。万一正解写挂了还能拿一点暴力分。 另外注意由于数据最多有 $10^6\times10^4=10^{10}$,需开long long。 此题最大的难点其实是写出转移方程,相较而言优化反而没有那么难了。 $$\texttt{The End.}$$