枚举行区间把二维压成一维:先用列前缀和得到每列在该行区间内的和,再对这一维数组跑 Kadane 求最大子段和,O(N³)。
OJ: roj
题目 ID: 1224
难度:普及+/提高-
标签:动态规划前缀和枚举python
创建: 2026-09-30 00:29
更新: 2026-10-07 12:15
形式化题目
给定一个
其中
以题面样例的
| 第 1 列 | 第 2 列 | 第 3 列 | 第 4 列 | |
|---|---|---|---|---|
| 第 1 行 | 0 | -2 | -7 | 0 |
| 第 2 行 | 9 | 2 | -6 | 2 |
| 第 3 行 | -4 | 1 | -4 | 1 |
| 第 4 行 | -1 | 8 | 0 | -2 |
表中加粗的
正解
思路
一句话本质:枚举上下边界把二维压成一维,对"每列在该行区间内的和"跑一遍一维最大子段和(Kadane),
先看最朴素的做法:枚举矩形的四条边——上边界
但矩形本身有
关键观察是:行区间一旦固定,列方向就只剩一个一维问题。对固定的行区间
那么选择列区间
其中
枚举
用样例把所有行区间的结果都算出来,可以看到
| 行范围(闭区间) | 该区间的列累加和 |
一维最大子段和 |
|---|---|---|
| 0 | ||
| 9 | ||
| 6 | ||
| 13 | ||
| 11 | ||
| 8 | ||
| 15 | ||
| 1 | ||
| 9 | ||
| 8 |
表中每一行对应一个行区间:第二列是"把该行区间内每一列压成一个数"得到的列累加和,第三列是对这个一维数组跑 Kadane 的结果。全部
最后放大看获奖的那一行区间
| 列 | 列累加和 |
|
|---|---|---|
| 1 | 4 | 4 |
| 2 | 11 | 15 |
| 3 | -10 | 5 |
| 4 | 1 | 6 |
第 1 列 acc = 4 表示从它开始;第 2 列接到后面得 acc 拖到
代码
best_segment 承担一维最大非空子段和;pref[c][t] 是第 col[j] - col[i] 即行区间 band 是惰性生成器,不额外开数组。
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 00:30
# update_at: 2026-09-30 00:30
import sys
from collections.abc import Iterable
from itertools import accumulate
def best_segment(nums: Iterable[int]) -> int:
"""一维最大非空子段和(Kadane),acc 表示"以当前元素结尾的最大和"。
转移 max(x, acc + x):前缀为负时从本元素重新开始,否则接在最优后缀后面。
"""
return max(accumulate(nums, lambda acc, x: x if x > acc + x else acc + x))
def max_submatrix(rows: list[list[int]]) -> int:
"""非空子矩阵的最大元素和:枚举行区间,把列方向交给一维最大子段和。
固定行区间 [i, j) 后每列之和是一个定值,列区间的最优选择只取决于这一维数组。
"""
n = len(rows)
# pref[c][t] 是第 c 列前 t 行之和,故列 c 在行区间 [i, j) 上的和 = pref[c][j] - pref[c][i]
pref = [tuple(accumulate((0, *col))) for col in zip(*rows)]
ans = pref[0][1] # 至少取一格:用它兜住矩阵全为负数的情形
for i in range(n):
for j in range(i + 1, n + 1): # 行区间 [i, j) 保证非空
band = (col[j] - col[i] for col in pref) # 该行区间下每列的累加和
ans = max(ans, best_segment(band))
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data) # N×N 的边长
rows = [[next(data) for _ in range(n)] for _ in range(n)]
print(max_submatrix(rows))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:29
* update_at: 2026-10-04 23:29
*/
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
const int MAXN = 105;
// col_pref[c][t] 表示第 c 列前 t 行之和(列和行都从 1 开始编号)
// 于是第 c 列在行区间 [i, j) 上的和 = col_pref[c][j] - col_pref[c][i]
ll col_pref[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
for (int r = 1; r <= n; r++) {
for (int c = 1; c <= n; c++) {
ll x;
cin >> x;
col_pref[c][r] = col_pref[c][r - 1] + x;
}
}
// 答案初值取一个真实单格和,保证矩阵全为负数时也能返回最大单格而不是 0
ll ans = col_pref[1][1];
// 枚举上下边界 i, j(行区间 [i, j) 非空),把二维压成一维
for (int i = 0; i < n; i++) {
for (int j = i + 1; j <= n; j++) {
// 对该行区间下"每列的和"这一维数组跑 Kadane
// acc 表示以当前列为右端点的最大非空子段和
ll acc = col_pref[1][j] - col_pref[1][i];
ll best = acc;
for (int c = 2; c <= n; c++) {
ll col_sum = col_pref[c][j] - col_pref[c][i];
// 要么从本列重新开始,要么接在以上一列为右端点的最优子段后面
acc = max(col_sum, acc + col_sum);
best = max(best, acc);
}
ans = max(ans, best);
}
}
cout << ans << endl;
return 0;
}复杂度
- 时间复杂度:
。列前缀和 ,枚举行区间 个、每个 Kadane 。 时约 次操作,对比暴力 的 低一个数量级;与 C++ 正解(枚举行区间 + 内层 Kadane)完全同阶。 - 空间复杂度:
,存放列前缀和 个整数;每个行区间的 band与accumulate都是惰性流,不建额外数组。实测峰值内存 16.0 MB(OJ 限 128 MB)。
总结
本题是"二维最大子矩阵"的模板题,核心动作只有一步:固定上下边界,把二维问题压成一维最大子段和。推导顺序是"暴力枚举四条边 ans 的初值不能取 0(矩阵全为负时答案应为最大单元素),代码里用 pref[0][1] 这个真实单格和兜底;二是 Kadane 的转移必须是
图示解析
这张图串起本题从建模到得到答案的主线:
N×N 矩阵 (N ≤ 100), 求非空子矩阵最大和
`- 朴素: 枚举上/下/左/右四条边, O(N⁴) 个矩形 (≈2.5×10⁷, Python 不可行)
`- 瓶颈: 列的两个端点 l, r 被当成独立枚举对象, 多花一维
`- 关键观察: 行区间 [i, j) 固定后, 列 c 的和 S_c = P_c[j] - P_c[i] 是定值
`- 降维: 矩形和 = S 的连续子段和 → 一维最大子段和 (Kadane)
`- 得到答案: O(N²) 个行区间 × O(N) 扫描 = O(N³)先看每个节点代表的推理结论,再顺着分支检查它如何导向下一步: