Luogu P6649 「SWTR-05」Grid
\(\text{题目链接}\)
\(Solution\)
提供一个 \(\mathcal{O}(nm\log m)\) 的做法。
题目转换一下,可以理解为从 \((1,1)\) 走到 \((n,m)\) ,每次走到一行的时候可以在前面多取一段连着的和的最小值。
把原矩阵全部取反就是最大值。
这样子转化问题就很明显了,就是一个经典问题:
求一个矩阵,只能向右向下走,求第一行走到最后一行的最大值,也就是设 \(f[i][j]\) 为走到 \((i,j)\) 的最大值。
为 \(f[i][j]=\max\{f[i][j-1]+a[i][j],f[i-1][j]+a[i][j]\}\) 加上了一个可以在前面多选一个以 \(j\) 为结尾的区间和的问题。
从 \(f[i][j-1]\) 转移过来就不需要再找前面一个在第 \(i\) 行以 \(j-1\) 为结尾的最大区间和了,因为 \(f[i][j-1]\) 已经计算了以 \(j-1\) 为结尾的最大区间和。
而对于从 \((i-1,j)\) 转移而来,需要求一个在第 \(i\) 行以 \(j\) 为结尾的最大区间和,可以利用前缀和,把前 \(j-1\) 所有前缀和放进小根堆里面,取堆顶即可。因为区间和 \(sum[j]-sum[k-1]\) 最大,需要 \(sum[k-1]\) 最小(\(sum[j]\) 代表第 \(i\) 行以 \(j\) 为结尾的前缀和,已经确定了)。
对以上两种情况分类取最大值即可。
PS: 可能人傻描述麻烦,可以理解为那个经典问题带一个可以加上一个区间和的转移,求这个值用堆贪心即可。
因为有个堆,时间复杂度 \(\mathcal{O}(nm\log m)\)(貌似被其他更优做法吊打了)
\(Code\)
#include<iostream>
#include<cstdio>
#include<queue>
#define ll long long
const int N = 1010;
inline ll Max(ll x, ll y) { return x > y ? x : y; }
inline ll Min(ll x, ll y) { return x < y ? x : y; }
inline ll read() {
ll r = 0; bool w = 0; char ch = getchar();
while(ch < '0' || ch > '9') {
if(ch == '-') w = 1;
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
r = (r << 3) + (r << 1) + (ch ^ 48);
ch = getchar();
}
return w ? ~r + 1 : r;
}
int n, m;
ll a[N][N], f[N][N], sum, ans = -0x7fffffffffffffff;
std::priority_queue<ll>q;
int main() {
n = read(); m = read();
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
a[i][j] = -read();
for(int i = 1; i <= m; ++i) f[1][i] = Max(f[1][i - 1] + a[1][i], a[1][i]);
for(int i = 2; i <= n; ++i) {
while(!q.empty()) q.pop();
q.push(- (sum = a[i][1]));
q.push(0);
f[i][1] = f[i - 1][1] + a[i][1];
for(int j = 2; j <= m; ++j) {
sum += a[i][j];
f[i][j] = Max(f[i][j - 1] + a[i][j], f[i - 1][j] + sum + q.top());
q.push(-sum);
}
}
for(int i = 1; i <= m; ++i) ans = Max(ans, f[n][i]);
printf("%lld\n", -ans);
return 0;
}