最大子矩阵和:从 O(n⁴) 到 O(n³) 的列压缩 + Kadane
一、题意
给定一个 n×n 的整数矩阵,元素在 -127, 127 之间,求元素和最大的子矩阵(子矩阵必须非空)。
n ≤ 100,样例矩阵:
diff
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
答案是 15,来自第 24 行、第 12 列那个 3×2 的子矩阵。
二、先看暴力:O(n⁴)
最直接的想法是枚举左上角和右下角,用二维前缀和 O(1) 查询矩形和:
c
for (int x1 = 1; x1 <= n; x1++)
for (int y1 = 1; y1 <= n; y1++)
for (int x2 = x1; x2 <= n; x2++)
for (int y2 = y1; y2 <= n; y2++) {
long long s = pos[x2][y2] - pos[x1-1][y2]
- pos[x2][y1-1] + pos[x1-1][y1-1];
if (s > best) best = s;
}
n = 100 时约 10^8 次运算,1 秒贴线能过,但常数稍大就危险。而且这题真正想考的不是这个。
写这版有三个经典坑:
best初值不能是 0。矩阵允许全负,答案可能是 -1,初值 0 会把它盖掉。- 左上角循环要
<= n,写成< n会漏掉最后一行/列。 - 右下角要从
x1、y1开始,才能枚举到 1×1 的子矩阵。
三、关键观察:固定上下边界,二维就变一维
固定行区间 [top, bottom],把每一列在这个行区间内的元素加起来,得到一维数组:
css
col[j] = a[top][j] + a[top+1][j] + ... + a[bottom][j]
于是「上下边固定在 top, bottom、左右边任取」的最大子矩阵和,就等于 col 数组的最大子段和。
枚举所有行区间(O(n²) 个),每个跑一遍最大子段和(O(n)),总复杂度 O(n³)。
而且 col 可以增量维护:bottom 每增加 1,只需要把新的一行加上去,不用重新求和。
四、Kadane:一维最大子段和
定义 cur 为「以当前位置 j 结尾的最大子段和」,则
scss
f(j) = max(col[j], f(j-1) + col[j])
含义很直白:要么从 j 重新开始,要么接在前一段后面,取大的那个。
c
int Kadane(int *a, int n) {
int best = a[1], cur = a[1];
for (int i = 2; i <= n; i++) {
cur = (cur + a[i] > a[i]) ? cur + a[i] : a[i]; // 接上 or 重启
if (cur > best) best = cur;
}
return best;
}
初值取 a[1] 而不是 0,这样全负数组也能返回最大单元素,不用额外特判。
五、完整代码
c
#include <stdio.h>
int Kadane(int *a, int n) {
int best = a[1], cur = a[1];
for (int i = 2; i <= n; i++) {
cur = (cur + a[i] > a[i]) ? cur + a[i] : a[i];
if (cur > best) best = cur;
}
return best;
}
int pos[105][105];
int main(void) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
scanf("%d", &pos[i][j]);
int col[105];
int ans = -1000000000;
for (int top = 1; top <= n; top++) {
for (int j = 1; j <= n; j++) col[j] = 0; // 换起点必须清零
for (int bottom = top; bottom <= n; bottom++) {
for (int p = 1; p <= n; p++) // 列循环,上界是 n
col[p] += pos[bottom][p];
int k = Kadane(col, n);
if (k > ans) ans = k;
}
}
printf("%d\n", ans);
return 0;
}
六、正确性
任意一个子矩阵都由它的行区间 [x1, x2] 和列区间 [y1, y2] 确定。外层枚举了全部 O(n²) 个行区间,对固定的 [top, bottom],col 恰好就是各列在该行区间内的和,而 Kadane 枚举了 col 的所有子段------也就是所有列区间的和。所以每个子矩阵都恰好被某个 (top, bottom, 列区间) 覆盖一次,最大值不会漏。
七、复杂度
- 时间:行区间 O(n²) × 每次 O(n) 的累加和 O(n) 的 Kadane → O(n³),n = 100 时约 10^6 次运算
- 空间:O(n²) 存原矩阵 + O(n) 的 col
比四重循环快两个数量级。
八、我踩过的三个坑
坑 1:把列循环的上界写成了行号。
c
for (int p = 1; p <= m; p++) col[p] += pos[m][p]; // 错
p 是列,上界必须是 n,不是当前行号 m。当 m < n 时,后面那些列根本没被累加,仍然残留着 0。
坑 2:残留的 0 被 Kadane 当成合法元素。
矩阵全负时,Kadane 会挑中这个"假 0",答案变成 0。最小反例:
diff
2
-1 -2
-3 -4
正确输出是 -1,错误代码输出 0。
坑 3:同一份结果算了两遍。
c
if (Kadane(col, n) > max) max = Kadane(col, n); // 调用了两次
先存到变量里再比较,省一半常数。
九、对拍验证
写一个 O(n⁶) 的暴力版(枚举四角 + 逐格求和),随机生成 60 组 n ≤ 6、元素 ∈ -9, 9 的数据对拍:
ini
FAIL = 0 / 60
同时样例输出 15、n=1 的 -5、全负 2×2 的 -1 都正确。
十、口诀
枚举行区间,压缩列成一维,跑最大子段和。
看到「最大子矩阵」四个字,脑子里直接反应这三个动作。行变量和列变量的上界千万别串号------这是这题最容易犯、也最难一眼看出的 bug。