最大子矩阵和:从 O(n⁴) 到 O(n³) 的列压缩 + Kadane

最大子矩阵和:从 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 秒贴线能过,但常数稍大就危险。而且这题真正想考的不是这个。

写这版有三个经典坑:

  1. best 初值不能是 0。矩阵允许全负,答案可能是 -1,初值 0 会把它盖掉。
  2. 左上角循环要 <= n,写成 < n 会漏掉最后一行/列。
  3. 右下角要从 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。

相关推荐
叼烟扛炮2 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain
§ꦿCFོ༉3 小时前
嵌入式八股文—C语言入门学习(二)
c语言·学习·嵌入式·编程语言
IT_陈寒3 小时前
SpringBoot实战技巧总结
前端·人工智能·后端
TeamDev3 小时前
使用 JxBrowser 在桌面应用中集成 Spring
java·前端·后端·spring·混合应用·jxbrowser·浏览器控件
陈随易4 小时前
Rolldown 把插件改动回传提速 26 倍
前端·后端·程序员
笃行35012 小时前
集中式与分布式,KingbaseES 的四种形态共用一套内核
前端
David猪大卫12 小时前
【Linux修炼】环境变量、进程地址空间
linux·c语言·经验分享·笔记·学习·ubuntu·centos
陈随易13 小时前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员
ZhengEnCi14 小时前
12 种高级 UI 交互效果及对应 AI 提示词:从按压回弹到底部抽屉的物理感教学
前端·css·交互设计