二维最大子矩阵和:枚举上下界与Kadane算法的完整实战解析

发布时间:2026/10/23 8:19:15

二维最大子矩阵和:枚举上下界与Kadane算法的完整实战解析 二维最大子矩阵和这个经典问题我在两组场景里都遇到过一次是刷算法题一次是业务中要在热力数据网格上找最大热点区域。大多数人的第一反应都是“枚举四条边界 二维前缀和加速求和”小矩阵上还能跑数据一旦到几百上千规模就彻底卡死。而枚举上下界 Kadane算法之所以能成为标准解法是因为它把“找子矩阵”拆成了两个层次先固定上、下两条行边界把二维矩阵压成一维的列和数组再在这个一维数组上跑 Kadane自动确定左、右边界。整体时间复杂度只有 O(m²n)空间 O(n)。这篇文章适合正在刷二维动态规划、准备算法面试或者需要在工程里做二维区间聚合的读者我会把从一维 Kadane 到二维坐标回溯的完整链路讲透并给出可直接复用的代码。1. 从一维到二维Kadane算法为什么能直接迁移1.1 一维Kadane算法不是“背模板”是动态规划最大连续子数组和Maximum Subarray是算法入门必刷题给定数组a找出一个非空连续子数组使其和最大。Kadane 算法的核心非常朴素——维护一个滚动变量cur它始终代表“以当前元素结尾的最大连续子数组和”。每扫到新元素a[i]只面临两个选择把a[i]接到前面那段连续区间后面cur a[i]放弃前面那段让a[i]重新作为起点a[i]于是状态转移就是一句话cur max(a[i], cur a[i])同时用ans记录历史所有cur的最大值扫完就得到答案。这个转移本质是动态规划设f[i]是以a[i]结尾的最大连续子数组和那么最优子结构就是“要么自己开头要么接在 f[i-1] 对应的区间后面”。这里的直觉值得多说一句如果cur已经是负数它作为“前缀”对后续元素只会拖后腿。比如你爬山前面一段净海拔已经是负的那后面想走高必然应该换起点而不是从更早的地方继续接。这也是“连续”这个约束下贪心与 DP 能同时成立的原因。很多资料把 Kadane 写成cur max(0, cur a[i])这其实是允许空子数组的版本全负数数组会返回 0。而二维最大子矩阵问题里题目几乎都要求非空子矩阵所以更稳妥的写法是cur max(a[i], cur a[i])。这个细节看起来小但全负矩阵的测试用例会立刻暴露问题后面第 3 节我专门展开。1.2 二维暴力解的空间到底有多大二维问题的定义是给定 m×n 矩阵每个元素有正有负求所有子矩阵中元素和的最大值。一个子矩阵由四条边界确定上边界top、下边界bottom、左边界left、右边界right。如果直接暴力枚举四条边界子矩阵总数是 O(m²n²) 种。就算提前构建二维前缀和把“求子矩阵和”这一步从 O(mn) 优化到 O(1)总复杂度仍然是 O(m²n²)。当 m n 100 时是 1e8 次操作勉强能忍到 m n 1000 就是 1e12任何语言都跑不动。如果不用二维前缀和每次求和全靠裸加复杂度直接是 O(m³n³)只能适配个位数规模的矩阵。为什么二维比一维难这么多一维的子区间只有左右两个端点二维的子矩阵却有四条边界组合数呈数量级爆炸。所以优化的方向必然只有一个减少需要显式枚举的边界数量。二维前缀和只优化了“求和”的常数没有减少边界组合所以它解决不了根本问题。1.3 固定行边界后列方向的连续区间问题标准解法只做了一件事只枚举top和bottom两条行边界然后把[top, bottom]这段行区间内每一列的元素累加成一个数得到长度 n 的一维数组colSumcolSum[j] mat[top][j] mat[top1][j] ... mat[bottom][j]此时问题变成了在colSum上找一个连续区间[left, right]使得区间和最大。因为上下边界已经固定任何left/right组合都唯一对应一个“上下边界恰好是 top/bottom”的子矩阵。而在一个一维数组上找最大连续区间和正是 Kadane 算法线性扫描能自动解决的事情。于是外层枚举上下边界的 O(m²) 次循环每次都调用一次 O(n) 的 Kadane总复杂度 O(m²n)。相比枚举四条边界的 O(m²n²)“左边、右边”这两维边界被压缩进了 Kadane 的线性扫描里。如果你面试时能把这个“为什么可以压缩维度”的逻辑讲清楚比单纯背下代码有价值得多。可以这样理解把矩阵想象成从上往下叠的一沓纸每固定一个上下层段就把这个层段在每一列的厚度叠成一条竖条然后在竖条上做“最大连续段”搜索。所谓最大子矩阵本质就是“在行方向上选一段层叠在列方向上选一段区间”。2. 枚举上下边界时的经典误区列和为什么要增量更新2.1 标准框架的伪代码标准流程可以用下面这段伪代码概括ans -inf for top in 0..m-1: colSum [0] * n # 每一轮 top 都要重置列和数组 for bottom in top..m-1: # 关键增量更新 colSum[j] mat[bottom][j] (j 0..n-1) # 在 colSum 上跑 Kadane更新 ans这里最容易被忽略也最值得展开的就是colSum的更新方式。当bottom从上一行下移到当前行时不需要重新计算top到bottom之间所有行的列和只需要把新进入行区间的这一行累加进colSum即可。因为上一次循环里的colSum已经表示top到bottom-1行的列和再叠加一行就精确变成了top到bottom行的列和。这个技巧之所以成立是因为我们的枚举顺序是从某个top开始把bottom依次向下扫行区间是“逐步扩张”的。后一个状态可以由前一个状态 O(n) 转移得到这就是最朴素的滚动累加思想。2.2 反模式一每轮重新累加行区间最容易写错、也最常见的低效写法是这种for top in range(m): for bottom in range(top, m): col_sum [0] * n for r in range(top, bottom 1): for j in range(n): col_sum[j] mat[r][j] # 又把 top..bottom 全扫一遍 kadane(col_sum)这个版本的问题在于每次bottom变化都要把top到bottom的整段行区间重新扫一遍来构建colSum。构建代价是 O((bottom - top 1) * n)把所有top/bottom组合加起来总复杂度是 O(m³n)。从 O(m²n) 退化成 O(m³n)听起来只多了一个 m但 m 1000 时就是 1e12 和 1e9 的区别本质上是“可跑”与“不可跑”的区别。2.3 反模式二二维前缀和逐列差分还有人会用列方向的前缀和来算col_sum[j] pre[bottom 1][j] - pre[top][j] # 每轮做 n 次差分严格说这个写法复杂度没有退化也是 O(m²n)。但它有两个代价需要额外维护一个 O(mn) 的二维前缀和数组每轮循环都必须把 n 列都做一次减法常数上比“只加新一行”的增量更新更大。如果你的矩阵是逐行读入的我更推荐增量更新代码更短、常数更小、不需要额外空间。除非你的输入不是按行顺序给的或者你确实需要频繁随机查询任意行区间的列和那种场景二维前缀和才有不可替代的优势。2.4 正确性与复杂度分析简单证明一下增量更新的正确性。记S(top, bottom, j)为第 j 列从第 top 行到第 bottom 行的元素和。外层固定 top 后初始colSum全 0当 bottom 从 b-1 变成 b 时S(top, b, j) S(top, b-1, j) mat[b][j]所以循环中的colSum[j] mat[bottom][j]这句操作维护的正是“当前 top 行到底行之间的第 j 列和”。bottom 每取一个值colSum 都是精确的 [top, bottom] 列和再跑 Kadane得到的就严格等于“上下边界分别为 top 和 bottom 时最优子矩阵的和”。复杂度账很好算外层 top 有 O(m) 种内层 bottom 平均 O(m/2) 种每次 bottom 循环里“累加新行”O(n) “Kadane 扫描”O(n)。总复杂度 O(m²n)空间只是一个长度 n 的colSum数组O(n)。注意如果矩阵是“高瘦型”即 n 远小于 m那么更优策略是反过来枚举左/右边界的组合对行做 rowSum 增量更新Kadane 在行方向上扫描复杂度变成 O(n²m)。核心原则是不要盲目固定方向看哪种枚举维度的组合数更小。这一点第 5 节我再补充。3. 完整实现与手动推演从Python到C的落地细节3.1 Python实现与初始化的坑先给一个最简的可运行版本只返回最大和def max_submatrix_sum(mat): m, n len(mat), len(mat[0]) if m 0 or n 0: return 0 ans float(-inf) for top in range(m): col_sum [0] * n for bottom in range(top, m): # 增量更新列和 for j in range(n): col_sum[j] mat[bottom][j] # 一维 Kadane非空子数组版本 cur 0 for val in col_sum: cur max(val, cur val) if cur ans: ans cur return ans初始化这里的几个细节建议死记ans用float(-inf)初始化。如果题目允许返回空矩阵即答案为 0改成ans 0也行但绝大多数“最大子矩阵和”题目要求子矩阵非空全负矩阵时正确答案应该是最大那个单一元素而不是 0。内层 Kadane 里cur初始化为 0 没有任何问题。因为第一次遇到元素val时cur max(val, 0 val) val起点自动从第一个元素开始了。千万不要把内层写成cur max(0, cur val)。这个写法返回的是“允许空区间”的最大和全负矩阵会让你得到 0而不是真实的负值最大元素。3.2 一个3×3矩阵手动走一遍理论说再多不如手算一个例子。用下面这个 3×3 矩阵[ 1, -2, 3] [ 4, -5, 6] [-7, 8, 9]把每一种 top/bottom 组合对应的 colSum 和 Kadane 结果列出来topbottomcolSumKadane最大和00[1, -2, 3]301[5, -7, 9]902[-2, 1, 18]1911[4, -5, 6]612[-3, 3, 15]1822[-7, 8, 9]17以 top0、bottom2 为例colSum [-2, 1, 18]。Kadane 扫描过程val -2cur max(-2, 0 (-2)) -2ans -2val 1cur max(1, -2 1) 1ans 1val 18cur max(18, 1 18) 19ans 19最终全局最大值是 19对应子矩阵是第 0~2 行、第 1~2 列-2, 3 -5, 6 8, 9手动验算(-23) (-56) (89) 1 1 17 19正确。这个推演也能解释一个常见困惑最大子矩阵不一定是“从某一行开始到某一行结束”时最大的那个而是在多个 top/bottom 组合的 Kadane 结果中取历史最大值。所以每轮循环都必须持续和全局 ans 比较。3.3 C实现、long long与浮点扩展竞赛或工程里用 C 时代码长这样#include vector #include algorithm #include climits using namespace std; long long maxSubMatrix(const vectorvectorlong long mat) { int m mat.size(); if (m 0) return 0; int n mat[0].size(); long long ans LLONG_MIN; for (int top 0; top m; top) { vectorlong long colSum(n, 0); for (int bottom top; bottom m; bottom) { for (int j 0; j n; j) { colSum[j] mat[bottom][j]; } long long cur 0; for (int j 0; j n; j) { cur max(colSum[j], cur colSum[j]); ans max(ans, cur); } } } return ans; }两个工程经验矩阵权值和上限不明时colSum、cur、ans一律用long long。用 int 的话cur colSum[j]在边界数据下很容易溢出而且这个问题通常要到测试大样例才会暴露。如果矩阵元素是浮点类型图像处理、信号处理很常见把long long换成double即可Kadane 的贪心逻辑完全一致。4. 从“求最大和”到“框出子矩阵”坐标回溯实战4.1 一维Kadane记录左右端点很多业务场景不能只返回一个最大和还得告诉下游“最大热点区域到底在哪”。这时候只维护cur和ans不够需要额外记录子数组的左右端点。关键技巧是维护一个“临时起点”变量def kadane_with_index(arr): best float(-inf) cur 0 l r 0 tmp_l 0 for i, val in enumerate(arr): if cur val val: cur cur val else: cur val tmp_l i if cur best: best cur l, r tmp_l, i return best, l, r这里的tmp_l只在“决定放弃之前的区间从当前元素重新开始”时才更新为当前下标。如果cur接上了前面的区间临时起点不用变。只有当cur刷新了best才把正式的端点l, r更新为(tmp_l, i)。4.2 合成二维坐标的完整代码把带坐标的一维 Kadane 嵌进 top/bottom 循环就可以同时拿到最大和与完整子矩阵边界def max_submatrix_with_position(mat): m, n len(mat), len(mat[0]) ans float(-inf) final_top final_left final_bottom final_right 0 for top in range(m): col_sum [0] * n for bottom in range(top, m): for j in range(n): col_sum[j] mat[bottom][j] best, left, right kadane_with_index(col_sum) if best ans: ans best final_top, final_bottom top, bottom final_left, final_right left, right return ans, (final_top, final_left, final_bottom, final_right)final_top和final_bottom来自外层枚举final_left和final_right来自 Kadane 返回的左右端点四者组合起来就是一个完整子矩阵的四个边界。调用方拿到(top, left, bottom, right)之后可以很方便地切片原矩阵做可视化或进一步计算。4.3 多个最优解的场景化取舍当矩阵里有多个子矩阵同时达到最大和时上面代码默认返回“第一个被扫描到”的最优解因为更新条件是严格大于best ans。这个行为通常够用但有些场景会有额外偏好如果希望返回“最后一个”最优解把改成即可如果希望返回“区间最长”的解Kadane 内部的更新条件需要增加长度比较cur best或者cur best and i - tmp_l r - l时更新如果希望返回“上下边界跨度最小”或“面积最小”的解则要在外层 top/bottom 循环里增加判断。需要特别提醒的是这些额外规则必须在 Kadane 内部的端点维护阶段就处理因为端点信息只在 Kadane 内部可见。等返回外层再补救很容易写出重复扫描数组的代码。5. 边界情况、复杂度对比与进阶变体5.1 必测的六类边界用例写完标准解法后建议至少用下面六类用例回归测试能挡掉 90% 的边界 bug用例类型示例期望结果说明单元素矩阵[[-5]]-5立刻暴露“全负返回0”的问题全负矩阵[[-1,-2],[-3,-4]]-1非空子矩阵要求单行矩阵[3, -1, 4]6退化为1D Kadane单列矩阵[[1],[2],[-3]]3退化为1D Kadane全正矩阵[[1,2],[3,4]]10答案是全矩阵之和含0矩阵[[0,-5],[0,3]]30元素参与取最大值尤其注意的是[[-5]]和[[-1,-2],[-3,-4]]这两个用例。很多网上流传的题解在这个用例上会返回 0就是因为内层 Kadane 误用了cur max(0, cur val)。不是所有场景都要求非空子矩阵所以写代码前一定要先和需求方确认空矩阵算不算一个合法“子矩阵”。5.2 枚举方向的选择与复杂度对照表把几种典型方法放在一起看复杂度高低一目了然方法时间复杂度空间复杂度mn1000 时的量级暴力枚举 裸求和O(m³n³)O(1)1e18完全不可行暴力枚举 二维前缀和O(m²n²)O(mn)1e12不可行枚举上下边界 KadaneO(m²n)O(n)1e9常数优化后可跑枚举左右边界 KadaneO(n²m)O(m)1e9取决于m/n工程上的朴素经验是“谁短枚举谁”。严谨一点比较m²n和n²m的大小约掉公因子mn后等价于比较m和n。比如 m 2000、n 20 时枚举左右边界是 O(20² × 2000) 8e5比枚举上下边界 O(2000² × 20) 8e7 快 100 倍。这种常量级优化在大型网格数据处理时效果非常明显。5.3 进阶面积不超过K的矩形最大和LeetCode 363“矩形区域不超过 K 的最大数值和”是标准解法最常见的变体。思路同样先枚举上下边界拿到 colSum但目标不再是“最大连续和”而是“最大但不超过 K 的连续和”。Kadane 的贪心在这个限制下会失效。原因很简单Kadane 每一步追求的是“当前局部最优”但带 K 上限时一个暂时的“次优前缀和”可能反而能组合出满足 K 限制的更优答案。正确做法是回到前缀和 有序集合固定 top/bottom 得到 colSum 后求前缀和数组 prefix其中prefix[i] sum(colSum[0..i-1])遍历右端点 r i 时需要找到已经出现过的prefix[l]l i满足prefix[l] prefix[i] - K且尽可能小这样prefix[i] - prefix[l]就尽可能接近 K用有序集合维护已扫描的prefix通过lower_bound(prefix[i] - K)快速查询。整体复杂度 O(m² n log n)。这个变体把“Kadane 的贪心”与“前缀和 二分查找”合在一起非常适合作为标准解法的延伸练习。5.4 进阶最大平均子矩阵、三维甚至更高维如果换成“最大平均子矩阵”问题要找到平均值最大的矩形区域可以二分答案 mid把矩阵每个元素都减去 mid然后跑一遍本文的标准解法求最大子矩阵和。如果最大和 ≥ 0说明存在平均值 ≥ mid 的矩形。总复杂度 O(m² n log(精度))。这个思路在图像处理找最亮区域、信号处理找能量密度最高的窗口时都有应用。更高维的“最大子长方体”问题原理完全一致先枚举 z 方向的两层边界把三维长方体压成二维矩阵再在这个二维矩阵上用枚举上下界 Kadane 继续压成一维最后 Kadane 收尾。本质就是“一层一层向下压维”每一层都借用一次枚举边界 线性扫描的组合维数越高越是重复套用这个模式。最后分享一个我自己的习惯拿到二维最大子矩阵相关的题永远先画一张“上下边界固定、列和数组”的抽象图把思路落在纸上再写代码。因为这个解法真正的难点不在 Kadane——它本身就是一维经典算法——而在枚举上下边界时能不能看清“列和是随着 bottom 滚动更新的”。只要你的代码没有在 bottom 循环里重新累加多行数据基本很难写错。编码完之后记得一定跑一遍单元素、单行、单列、全负这几个测试用例能挡掉大部分边界 bug。这套“枚举上下界 Kadane”的模式值得你从今天开始纳入自己的算法工具箱。
延伸阅读

更多相关文章

2026/10/23 8:14:15

VSCode 集成 IAR:嵌入式开发更高效的编辑与调试工作流

1. 为什么把 VSCode 和 IAR 放在同一个工作流里 1.1 编辑器体验的差距,确实让人难受 做嵌入式开发的朋友应该都有这种经历:工程原本是用 IAR Embedded Workbench 建起来的,编译、下载、跑仿真都在这一个环境里完成,整个生命周期也…

2026/10/23 8:14:15

基于STM32的四路称重与MQTT超重检测系统设计

简介:面向学习STM32嵌入式开发与公路超重检测的读者,这份工程资料围绕四路称重采集、数据处理、OLED实时显示、阈值声光报警及联网上报展开,适合课程设计、毕业设计或物联网监测项目参考。压缩包共306个文件,约10.31MB&#xff0c…

2026/10/23 8:14:15

高速PCB阻抗测试全解析:IPC标准与TDR测试方法实战指南

1. 为什么阻抗测试标准这么重要做高速PCB设计的工程师,几乎都经历过这样的场景:板子贴上“阻抗”两个字,工厂回传的测试报告却让人看得一头雾水。要么是测试方法跟你想象的不一样,要么是判定限值跟设计文件对不上,更常…

2026/10/23 11:30:39

计算机考研大纲深度拆解:从考纲到复习地图的实战指南

1. 一份考纲PDF背后的信息差每年九月前后,计算机专业的备考圈子里总会流传一份文件,文件名通常长这样:2019年计算机考研大纲(原文).pdf。很多人第一次拿到它,第一反应是“就这?”——薄薄几十页,排版朴素&a…

2026/10/23 11:30:39

SpringBoot 连接带密码 Access 数据库:UCanAccess 实战与避坑指南

1. 为什么还要折腾 SpringBoot 连 Access先把结论摆在前面:SpringBoot 连 Access 这件事,在今天的生产环境里绝对不是主流选择,但它真实存在,而且需求比很多人想象的要顽固。我最近就碰到一个场景,某公司内部有一套跑了…

2026/10/23 11:30:39

C语言模拟操作系统:从时间片轮到LRU,一份可运行的课设全解析

简介:东北大学操作系统课程设计完整实现,围绕进程管理、内存管理、文件系统与设备管理四大核心模块展开,适合计算机专业学生作为课设参考,也可用于操作系统原理的实践复习。资源共24个文件,主体为21个C源文件&#xff…

2026/10/23 11:30:39

iptables.service找不到?深入解析Linux防火墙管理的范式迁移

1. 项目概述:为什么一条报错提示能暴露系统管理的底层逻辑“Redirecting to /bin/systemctl status iptables.service Unit iptables.service could not be found”——这行看似简单的终端输出,其实是Linux系统演进过程中一个极具标志性的“时代断层线”…

2026/10/23 11:30:39

Lenovo Vantage 4.10.371.0离线安装与排错指南

简介:联想Vantage V4.10.371.0 系统之家整合包面向使用联想电脑、希望集中完成驱动更新与设备维护的用户,提供硬件检测、系统优化、安全防护与驱动管理等常用入口,适合想快速部署官方管理工具、减少逐项查找驱动成本的日常使用场景。压缩包共…

2026/10/23 11:25:36

Excel按行拆分实战:openpyxl流式脚本与数据防失真

1. 需求场景拆解:什么情况下非要把 Excel 切开1.1 四种高频切分场景,看看你中了几个Excel 按行切分这件事听着像小需求,但只要你在数据岗待过一段时间,就会发现它几乎是每个月底的固定动作。我把过去几年遇到过的情况归了归类&…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/23 0:02:26

Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战

说实话,这几年接触了不少想入门FPGA的朋友,如果你问哪个项目能一口气把Verilog的核心基本功串起来,我通常会推荐电子密码锁。这个项目看着不起眼,却把矩阵键盘扫描、按键消抖、状态机设计、动态显示刷新、外设控制这些数字逻辑里的…

2026/10/23 0:02:26

数据服务监控运维实战:从指标体系到告警治理与故障排查

周末凌晨两点,手机震了。第一反应是“数据服务挂了”。打开电脑看了下,果然,某张核心报表的数据产出时间已经超过SLA两小时,下游三个业务方连着发来问询。这种场景,做过大数据运维的人都不会陌生。数据服务的监控与运维…

2026/10/23 0:02:26

FreeRTOS任务通知实战详解:内核原理、API与避坑指南

裸机编程切到RTOS之后,任务之间的通信和同步永远是躲不开的坎。前面聊过队列、信号量、互斥锁、事件组,这次把FreeRTOS任务通知(Task Notification)单独拎出来讲透。原因很简单:任务通知是FreeRTOS里最容易用错、也最容…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑