1. 这是一道什么题读懂“最大子矩阵”背后的真实考点考研复试机试里有一类题平时刷着不起眼真到了考场上却容易让人卡住“最大子矩阵”就是典型代表。这道题出自一所顶尖高校的复试机试题表面上只是要求从一个矩阵中找一个子矩阵使元素和最大实际上考的是三件事动态规划的变形应用、二维问题转一维问题的思维、以及边界情况的处理能力。先看题目本身。给定一个 n×n 的矩阵部分版本是 n×m每个位置上有一个整数可能为正、为负、为零要求找出一个连续的子矩阵使得这个子矩阵内所有元素的和最大输出这个最大值。举个例子0 -2 -7 0 9 2 -6 2 -4 1 -4 1 -1 8 0 -2这里最大值是 15取自右下角的子矩阵 [[9,2],[-4,1],[-1,8]]其实是从第二行到第四行、第二列到第三列组成的矩阵2 -6 1 -4 8 0等等——这个矩阵和是 1显然不对。正确答案是选择第二列和第三列之间的区域也就是9 2 -4 1 -1 8和为 92(-4)1(-1)8 15。这类题目如果第一次见很容易陷入“挨个枚举左上角和右下角”的暴力思路然后发现复杂度高得离谱代码也越写越乱。这篇文章面向的目标读者很明确正在准备复试机试的同学、想把动态规划基础打牢的算法学习者、以及面试前想快速过一遍经典题的工程师。我会从一维问题讲起逐步把二维问题的解法推出来再给出可直接照抄的实现代码最后结合考场经验聊聊哪些坑最容易踩。这题本身不难但想一次写对、不超时背后那几步转化思维值得好好拆一遍。2. 从一维到二维核心转化思路2.1 先搞定一维版本最大子数组和二维最大子矩阵问题本质是一维最大子数组和的扩展。所以别急着看二维先把一维问题彻底吃透。一维问题是给定数组 a[0..n-1]求一个连续子数组使其和最大。经典做法是 Kadane 算法扫描一遍数组维护两个变量cur表示以当前位置结尾的最大子数组和ans表示扫描到当前位置时出现过的最优答案状态转移非常简洁cur max(a[i], cur a[i]) ans max(ans, cur)注意这个转移里的细节如果cur a[i]比单独的a[i]还小说明之前的累计是负收益不如直接从当前元素重新开始。所以本质上 Kadane 算法在不断地做“是延续之前的子数组还是开启新子数组”的决策。用几组数据验证一下。数组[-2, 1, -3, 4, -1, 2, 1, -5, 4]i0: cur-2, ans-2i1: curmax(1,-1)1, ans1i3: curmax(4, -34)4, ans4i5: cur3, ans4? 这里要仔细算i4时 cur3, ans4i5时 curmax(-1, 32)5, ans5i6时 cur6, ans6最终结果是 6对应子数组[4, -1, 2, 1]正确。这个算法的时间复杂度是 O(n)空间 O(1)。相比暴力枚举所有起点终点 O(n²)优势非常明显。一维版本理解透了二维问题就有了抓手。2.2 二维问题的关键把行“压”成一维现在回到二维。矩阵里任意一个子矩阵都由上边界、下边界、左边界、右边界四个参数确定。最暴力的做法枚举上下左右四个边界再累加内部元素需要 O(n⁴) 甚至 O(n⁶) 的时间n 稍大一点就原地爆炸。核心转化思路是先枚举子矩阵的上边界 i 和下边界 j然后把第 i 行到第 j 行的每一列分别累加得到一个一维数组。举个例子对于前面那个矩阵如果上边界是第二行、下边界是第三行那么第一列的累加值是 9 (-4) 5第二列是 2 1 3第三列是 -6 (-4) -10第四列是 2 1 3得到一维数组[5, 3, -10, 3]。接下来对累加出来的一维数组跑 Kadane 算法得到的最大子段和就是在“上边界 i、下边界 j”这个固定行区间内所有可能的左右边界组合里取得的最大子矩阵和。外层再枚举所有可能的上下边界组合就能覆盖所有子矩阵。为什么这个转化是正确的因为左右边界的选择等价于在一维数组上选一个连续区间而在上下边界固定后每一列的和是唯一确定的子矩阵的和就等于这些列和在一个连续区间上的累加。于是“二维选一个矩形”变成了“一维选一个区间”维度降下来了复杂度也从 O(n⁴) 降到了 O(n³)。用生活化的类比理解想象每一列是一摞叠起来的砖块上下边界一夹就只取这摞砖块中间的一段把这段每一列的重量相加得到一串重量值然后在这串重量值里找“连续最重的一段”。这样视角一换问题瞬间清爽。2.3 两个版本复杂度对比暴力与优化先说最粗暴的枚举枚举矩形的左上角和右下角需要 O(n⁴) 种组合再求和一次 O(n²)总复杂度 O(n⁶)。n50 时已经是天文数字更不用说 n100 甚至 200。常见优化是二维前缀和。预处理一个sum[i][j]表示从 (1,1) 到 (i,j) 的子矩阵总和之后任意子矩阵求和降到 O(1)枚举上下左右边界仍是 O(n⁴)所以整体 O(n⁴)。n100 时大约需要 1 亿次操作仍很吃力。而行压缩的做法枚举上下边界 O(n²)对每个行区间累加一列 O(n)再跑 Kadane O(n)总复杂度 O(n³)。n100 时大约是 100 万次操作毫秒级完成。方案复杂度n100 时估算是否推荐暴力枚举 累加O(n⁶)远超可接受范围不推荐二维前缀和 枚举O(n⁴)约 1 亿次操作勉勉强强行压缩 KadaneO(n³)约 100 万次操作强烈推荐有些同学可能会问为什么不用二维前缀和直接枚举因为前缀和虽然把单次求和的成本降到 O(1)但 O(n⁴) 的枚举规模仍然太大。机试环境的时限通常 1 秒左右n 取到 100 时 O(n⁴) 就可能超时所以 O(n³) 是这道题比较稳妥的答案。3. 完整实现与关键细节3.1 基于行压缩的 C 实现直接给出可以在 OJ 上运行的完整代码。整体思路是先读入矩阵外层枚举上边界内层枚举下边界逐列累加得到一维数组再调用 Kadane 求最大子段和。#include cstdio #include algorithm #include climits using namespace std; const int MAXN 105; int a[MAXN][MAXN]; int temp[MAXN]; // 保存当前上下边界下每一列的累加和 // 一维最大子数组和Kadane算法 // 注意数组允许全为负数时也要返回最大的那个负数 int kadane(int arr[], int n) { int cur arr[0]; int ans arr[0]; for (int i 1; i n; i) { cur max(arr[i], cur arr[i]); ans max(ans, cur); } return ans; } int main() { int n; while (scanf(%d, n) ! EOF) { for (int i 0; i n; i) { for (int j 0; j n; j) { scanf(%d, a[i][j]); } } int ans INT_MIN; // 枚举上边界 for (int top 0; top n; top) { // 每换一个上边界temp 清空 fill(temp, temp n, 0); // 枚举下边界 for (int bottom top; bottom n; bottom) { // 把第 bottom 行累加进 temp for (int col 0; col n; col) { temp[col] a[bottom][col]; } // 对 temp 求最大子段和更新答案 ans max(ans, kadane(temp, n)); } } printf(%d\n, ans); } return 0; }这段代码把“行压缩”的过程内联在上下边界枚举里temp数组在每次上边界变化时重置下边界逐行往下扩展边扩展边累加不需要每次单独重新算一遍列和省掉了额外一层循环。3.2 关键步骤逐行解读第一个关键点是temp数组的维护方式。这里没有用temp[col] a[bottom][col]叠加前先对每一列做一次完整求和而是利用了下边界单调递增的性质固定上边界top后下边界从top一路走到n-1每次只需把新加入的那一行的元素累加到temp中就能得到当前上下边界区间内的列和。这比每次都重新求和快了一个常数级也不容易出错。第二个关键点是kadane函数的初始化。之前提到过Kadane 算法对全负数组要返回最大的负数所以cur和ans都初始化为arr[0]而不是 0。如果把ans初始化为 0遇到矩阵全为负的情况程序会输出 0而正确结果应该是最大的那个负数比如矩阵[-1, -2]的最大子矩阵和是 -1不是 0。这个细节在机试里非常容易丢分。第三个关键点是fill(temp, temp n, 0)的位置。重置必须在每次上边界改变后、开始累加新下边界之前执行。如果把重置放在双重枚举之外temp会残留上一轮的数据导致列和计算错误。我在初学这题时就因为在top循环外初始化temp结果样例怎么对都不对排查了很久才发现是残留数据在捣乱。第四个关键点是上下边界的枚举范围。bottom从top开始保证行区间非空这符合子矩阵“至少包含一行”的定义。可能会有一些题目允许空子矩阵但机试通常不允许所以不需要额外考虑空矩阵的情况。3.3 复杂度与正确性论证这部分建议在考场上有时间的话自己推一遍一是为了防止记错代码逻辑二是可以把复杂度分析写在注释里提醒自己。总量级计算如下外层top有 n 种选择内层bottom最多也有 n 种内部循环列累加 O(n)kadaneO(n)。所以总复杂度是 O(n³)。空间上只额外用一个长度为 n 的temp数组O(n)。关于正确性可以从两方面论证。覆盖性任意一个子矩阵一定存在某个上边界top和下边界bottom使得它的行区间被枚举到在对应该行区间时temp中保存的列和就是该子矩阵各列的和而一维 Kadane 会找到所有连续列区间中的最大值也就覆盖了任意左右边界。最优性一维 Kadane 本身是一维最大子数组问题的最优解因此对每个固定行区间都能找到该区间内最好的左、右边界全局取最大值即可得到答案。这两个论证在面试或复试现场如果被追问能清晰地讲出来远比“我背过这个模板”有说服力。4. 考场抗坑指南与常见问题排查4.1 最容易踩的五个坑这题看着不难但机试环境下一遍写对其实有难度。我结合自己和身边同学的经验整理了五个高频丢分点每一个都值得单独记下来。一是全负矩阵的特判。前面已经提到过kadane的初值必须取arr[0]否则遇到全负矩阵会错误地输出 0。可以这么记任何子矩阵都至少包含一个元素不存在“空子矩阵”这种默认情况。二是ans的初始值。ans要初始化为一个足够小的数比如INT_MIN而不是 0。因为在计算过程中临时结果可能一直为负如果初始值是 0最后输出会变成 0掩盖了真实的最大值。三是temp重置的位置。固定上边界后temp每个元素都必须从 0 开始。很多人会在top循环外初始化一次temp然后内层直接累加结果第二组上边界的数据里混入了第一组残留题目样例能过但大数据一测就废。四是读入方式。机试输入经常是多组数据直到 EOF 结束。while (scanf(%d, n) ! EOF)这个写法要形成肌肉记忆不要只读一组就结束。同时矩阵元素读入用scanf比cin更快除非你调用了ios::sync_with_stdio(false)否则在大输入规模下可能拖慢速度。五是索引边界。所有循环都从 0 开始到n-1结束。如果把bottom从top1开始会漏掉“只有一行”的子矩阵直接丢分。这类边界错误最好通过小矩阵自测来捕捉。4.2 自测用例清单考场时间紧张但至少用下面几组数据验证代码。我把测试用例整理成一个速查表做题时可以直接对照测试场景输入期望输出单个元素1且值为-5-5全负矩阵2-1 -2 -3 -4-1所有元素为正21 2 3 410经典样例上述 4×4 矩阵15矩阵中单个负元素夹在两个正元素之间1 -100 11两行混合正负-1 -1 -1 -1 -1 -1-1其中“单个负元素夹在两个正元素之间”这类用例特别能检测 Kadane 的实现是否正确。如果cur a[i]写成了cur直接加a[i]而不取max这道用例立刻就会暴露问题。4.3 现场调试技巧复试机试的调试环境和平时自己本地 IDE 有很大不同通常只有简单的输出打印没有断点调试。所以建议养成两种调试习惯。第一小数据对拍。写一个 O(n⁶) 的暴力版本和优化版本一起跑随机小矩阵对比输出。这是我在刷题时纠正错误最快的办法。暴力版虽然慢但 n 取 5 以内时速度完全够用可以作为判题基准。第二打印中间数组。在关键位置输出temp数组观察列和是否符合预期。比如上边界是 1、下边界是 2 时temp应该等于第 1 行的每列元素加上第 2 行的每列元素。如果发现temp不对劲多半是重置位置写错了而不是 Kadane 的问题。调试时还有一个用得到的原则把kadane单独抽出来测。如果你怀疑一维算法有误直接在main里构造一个一维数组调用kadane打印结果。这能快速区分到底是“二维转一维”写错了还是“一维最优解”求错了。5. 从这道题延伸到复试机试准备5.1 一类变体定长子矩阵与最大权子矩阵复试机试很少只考一道孤立的题往往会在原题基础上改动条件。掌握最大子矩阵后建议顺手看几个常见变体它们的核心解法高度相似。变体一是固定行数或列数。比如限定子矩阵必须包含恰好 k 行问最大和是多少。这时候仍然枚举上下边界但如果行数限定为 k可以直接枚举所有长度为 k 的行区间列累加后跑 Kadane复杂度不变。变体二是最大权子矩阵。矩阵每个格子有一个权值可能有正负解法完全相同因为权值只是把格子里的整数替换成了其他整数题目本质没有改变。变体三是找最小面积的最大和子矩阵。这类题可能会附加“如果最大值相同取面积最小”的条件需要额外记录面积。一般做法是在 Kadane 里同步维护每个最大子段对应的区间长度再和全局答案比较如果和相等则取更小面积。变体四是子矩阵元素和等于某个固定值。这种情况下可以把每个子矩阵的和通过前缀和计算出来用哈希表记录“某个行区间内出现过的列前缀和”将问题转化为两数之和类的查找。这个变体思路稍绕但它能帮你更深刻地理解行压缩这种预处理方式的威力。5.2 机试实战建议三遍法最后聊聊复试机试本身的应试策略。我个人的经验是拿到一道题先花三分钟想清楚再动手比自己边写边想效率高得多。具体做法可以称为“三遍法”。第一遍读题并标注关键信息。比如“n 最大是多少”“元素是否可能为负”“是否有多个测试用例”这三项直接决定算法选型和边界处理。最大子矩阵这题如果题目没说明输入到文件结尾一定要默认有多组数据宁可多写一个while(scanf()!EOF)也不能少写。第二遍估算复杂度并选定方案。n 在 100 以内O(n³) 可行n 在 500 以上O(n³) 就有些悬了可能需要借助单调栈等技巧处理特殊形态的矩阵。你要在动笔之前对复杂度和输入规模做到心中有数不要写完才发现超时。第三遍写代码并刻意检查边界。我后来养成一个习惯代码刚写完不急着提交先逐行走一遍边界逻辑——top0, bottom0是不是单行子矩阵bottomtop时temp是否已经包含了正确内容全负矩阵输出是否符合题目要求这些检查通常只需要一两分钟却能省下多次提交被罚的时间。这三遍法不只适用于这道最大子矩阵题它几乎可以套用到所有复试机试题目上。养成这个习惯之后我在模拟测试里的一次通过率明显提升了。说一个我个人在复习时的体会最大子矩阵这道题第一次接触确实会有点绕因为它的核心是把二维数组变成一维数组属于那种“想通了就很简单、想不通就很玄学”的题目。但一旦你亲手推几遍行压缩的过程再写几次代码这个转化思维就会被牢牢刻进脑子里之后遇到类似的矩形、区间、子数组问题都能很自然地往这个方向联想。如果你正在准备复试机试建议把这题作为掌握动态规划思维的一个里程碑来对待搞懂这一题很多二维 DP 题都会豁然开朗。 SEO 优化官网定制响应式建站教育培训建站