1. 问题背景与核心挑战
LeetCode 3070题要求我们统计所有元素和小于等于k的子矩阵数量。这道题在周赛中出现时,让不少选手感到棘手——因为它不仅考察基础的二维数组处理能力,更考验对前缀和技巧的灵活运用。我在第一次尝试时,就掉进了暴力枚举所有子矩阵的陷阱,导致时间复杂度爆炸。
这个问题的本质,是在一个二维矩阵中高效地计算任意矩形区域的和。想象你面前有一个Excel表格,需要快速知道任意矩形区域内数字的总和是多少。最直观的做法当然是每次都重新相加,但当表格很大、查询很频繁时,这种方式的效率就会变得极低。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二维前缀和:从一维到二维的思维跃迁
2.1 一维前缀和的回顾
我们先从更简单的一维情况说起。对于数组nums = [1,2,3,4,5],它的前缀和数组prefix就是[0,1,3,6,10,15]。这个技巧让我们能在O(1)时间内计算任意子数组的和:
python复制sum(nums[i..j]) = prefix[j+1] - prefix[i]
2.2 二维前缀和的构建
将这个思路扩展到二维,我们需要构建一个前缀和矩阵prefix,其中prefix[i][j]表示从矩阵左上角(0,0)到(i-1,j-1)这个矩形内所有元素的和。构建过程可以这样实现:
python复制def build_prefix(matrix):
m, n = len(matrix), len(matrix[0])
prefix = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
prefix[i][j] = matrix[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]
return prefix
这个递推公式的核心思想是:当前矩形的前缀和 = 当前元素 + 上方矩形和 + 左方矩形和 - 左上角重复计算的部分。
2.3 子矩阵和的快速计算
有了前缀和矩阵后,计算任意子矩阵(r1,c1)到(r2,c2)的和就变得非常简单:
python复制def query(prefix, r1, c1, r2, c2):
return prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1]
这个公式可以理解为:大矩形减去上方和左侧的多余部分,再加回被重复减去的左上角。
3. 暴力解法与优化思路
3.1 四重循环的暴力解法
最直观的解法是枚举所有可能的子矩阵,然后计算它们的和:
python复制def countSubmatrices(matrix, k):
m, n = len(matrix), len(matrix[0])
count = 0
for r1 in range(m):
for c1 in range(n):
for r2 in range(r1, m):
for c2 in range(c1, n):
total = 0
for i in range(r1, r2+1):
for j in range(c1, c2+1):
total += matrix[i][j]
if total <= k:
count += 1
return count
这个解法的时间复杂度高达O(m²n²),在矩阵较大时完全不可行。
3.2 使用前缀和优化
引入前缀和后,我们可以将内层的两个循环优化掉:
python复制def countSubmatrices(matrix, k):
m, n = len(matrix), len(matrix[0])
prefix = build_prefix(matrix)
count = 0
for r1 in range(m):
for c1 in range(n):
for r2 in range(r1, m):
for c2 in range(c1, n):
total = query(prefix, r1, c1, r2, c2)
if total <= k:
count += 1
return count
这样时间复杂度降到了O(m²n²),但仍然不够高效。
4. 进一步优化:固定边界与双指针
4.1 固定上边界的优化思路
观察到子矩阵必须包含(0,0)这个特殊条件(根据LeetCode题目描述),我们可以固定上边界为0,只考虑从(0,0)到(i,j)的子矩阵:
python复制def countSubmatrices(matrix, k):
m, n = len(matrix), len(matrix[0])
prefix = build_prefix(matrix)
count = 0
for i in range(m):
for j in range(n):
total = query(prefix, 0, 0, i, j)
if total <= k:
count += 1
return count
这样时间复杂度降到了O(mn),已经足够通过本题。
4.2 更一般情况的优化
如果题目不限定子矩阵必须从(0,0)开始,我们可以采用更复杂的优化策略。比如固定上边界,然后使用滑动窗口或双指针技巧:
python复制def countAllSubmatrices(matrix, k):
m, n = len(matrix), len(matrix[0])
prefix = build_prefix(matrix)
count = 0
for top in range(m):
for bottom in range(top, m):
left = 0
current_sum = 0
for right in range(n):
# 计算当前列的和
col_sum = query(prefix, top, right, bottom, right)
current_sum += col_sum
# 移动左指针
while current_sum > k and left <= right:
remove_col_sum = query(prefix, top, left, bottom, left)
current_sum -= remove_col_sum
left += 1
# 统计有效子矩阵
if current_sum <= k:
count += (right - left + 1)
return count
这个解法的时间复杂度是O(m²n),适用于更一般的情况。
5. 代码实现与边界处理
5.1 完整AC代码
结合题目要求,最终的Python实现如下:
python复制class Solution:
def countSubmatrices(self, grid: List[List[int]], k: int) -> int:
m, n = len(grid), len(grid[0])
prefix = [[0]*(n+1) for _ in range(m+1)]
count = 0
for i in range(1, m+1):
for j in range(1, n+1):
prefix[i][j] = grid[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]
if prefix[i][j] <= k:
count += 1
return count
5.2 边界情况处理
在实际编码中,有几个边界情况需要特别注意:
- 空矩阵处理:虽然题目保证矩阵非空,但良好的习惯是检查输入
- 整数溢出:当k很大时,前缀和可能超过整数范围,但在Python中这不是问题
- 单元素矩阵:最小的子矩阵就是单个元素本身
- k为负数的情况:根据题意,k≥0,但实际中可以增加检查
6. 复杂度分析与优化对比
6.1 时间复杂度分析
我们实现的最终解法:
- 构建前缀和矩阵:O(mn)
- 查询和统计:O(mn)
- 总时间复杂度:O(mn)
空间复杂度:O(mn)用于存储前缀和矩阵
6.2 不同解法的性能对比
| 解法类型 | 时间复杂度 | 适用场景 |
|---|---|---|
| 暴力枚举 | O(m²n²) | 仅适用于极小矩阵 |
| 前缀和优化 | O(m²n²) | 比纯暴力稍好 |
| 固定起点优化 | O(mn) | 题目限定起点时 |
| 双指针优化 | O(m²n) | 一般情况 |
7. 实际应用与扩展思考
7.1 图像处理中的应用
二维前缀和在图像处理中有广泛应用,比如:
- 快速计算图像某区域的像素强度总和
- 实现均值滤波、高斯模糊等卷积操作
- 图像特征提取时的积分图计算
7.2 扩展到更高维度
前缀和思想可以推广到三维甚至更高维度。例如三维前缀和用于计算立方体区域的和:
python复制prefix[i][j][k] = val + prefix[i-1][j][k] + prefix[i][j-1][k] + prefix[i][j][k-1]
- prefix[i-1][j-1][k] - prefix[i-1][j][k-1] - prefix[i][j-1][k-1]
+ prefix[i-1][j-1][k-1]
7.3 相关题目推荐
为了巩固这个技巧,建议尝试以下LeetCode题目:
-
- 二维区域和检索 - 矩阵不可变
-
- 矩形区域不超过 K 的最大数值和
-
- 元素和为目标值的子矩阵数量
-
- 统计全 1 子矩形
8. 常见错误与调试技巧
8.1 典型错误模式
在实现过程中,我遇到过以下几种常见错误:
- 前缀和矩阵下标错位:忘记前缀和矩阵比原矩阵多一行一列
- 边界条件处理不当:特别是当子矩阵包含第一行或第一列时
- 递推公式符号错误:容易混淆加减的顺序
- 空间优化时的覆盖问题:如果尝试原地计算前缀和
8.2 调试建议
当你的代码出现问题时,可以:
- 打印出构建的前缀和矩阵,手动验证几个简单case
- 对于小矩阵(如2x2),完整列出所有子矩阵并计算其和
- 使用assert语句检查中间结果
- 对比暴力解法的结果,找出第一个不一致的case
9. 空间优化技巧
9.1 行前缀和优化
如果不需要频繁查询任意子矩阵,可以只计算每行的前缀和:
python复制row_prefix = [[0]*(n+1) for _ in range(m)]
for i in range(m):
for j in range(n):
row_prefix[i][j+1] = row_prefix[i][j] + grid[i][j]
这样空间复杂度降为O(mn),查询任意子矩阵需要O(m)时间。
9.2 滚动数组技巧
对于逐行处理的情况,可以只维护前一行的前缀和:
python复制prev_row = [0]*(n+1)
for i in range(m):
curr_row = [0]*(n+1)
for j in range(n):
curr_row[j+1] = curr_row[j] + grid[i][j]
# 使用prev_row和curr_row进行计算
prev_row = curr_row
这样空间复杂度降为O(n)。
10. 不同语言实现的注意事项
10.1 C++实现要点
在C++中需要注意:
- 使用vector<vector
>存储矩阵 - 注意整数溢出问题,必要时使用long long
- 可以优化内存访问模式以提高缓存命中率
cpp复制int countSubmatrices(vector<vector<int>>& grid, int k) {
int m = grid.size(), n = grid[0].size();
vector<vector<int>> prefix(m+1, vector<int>(n+1));
int count = 0;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
prefix[i][j] = grid[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1];
if (prefix[i][j] <= k) ++count;
}
}
return count;
}
10.2 Java实现差异
Java实现时要注意:
- 数组初始化为0
- 使用System.arraycopy进行优化
- 同样需要注意整数溢出
java复制public int countSubmatrices(int[][] grid, int k) {
int m = grid.length, n = grid[0].length;
int[][] prefix = new int[m+1][n+1];
int count = 0;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
prefix[i][j] = grid[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1];
if (prefix[i][j] <= k) count++;
}
}
return count;
}
11. 测试用例设计指南
11.1 典型测试用例
设计测试用例时应考虑:
- 最小矩阵:1x1矩阵
- 单行或单列矩阵
- 所有元素相同的情况
- 元素值差异大的情况
- k的边界值:0、最大可能和等
11.2 示例测试用例
python复制def test():
sol = Solution()
# 单元素矩阵
assert sol.countSubmatrices([[5]], 5) == 1
assert sol.countSubmatrices([[5]], 4) == 0
# 单行矩阵
assert sol.countSubmatrices([[1,2,3]], 3) == 2
# 常规矩阵
assert sol.countSubmatrices([[1,2,3],[4,5,6]], 10) == 4
# 所有子矩阵都满足
assert sol.countSubmatrices([[1,1],[1,1]], 10) == 6
# 没有子矩阵满足
assert sol.countSubmatrices([[10,20],[30,40]], 5) == 0
print("All tests passed!")
12. 竞赛中的实战技巧
12.1 快速编码模板
在竞赛中可以准备前缀和的代码模板:
python复制class PrefixSum2D:
def __init__(self, matrix):
m, n = len(matrix), len(matrix[0])
self.prefix = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
self.prefix[i][j] = matrix[i-1][j-1] + self.prefix[i-1][j] + self.prefix[i][j-1] - self.prefix[i-1][j-1]
def query(self, r1, c1, r2, c2):
return self.prefix[r2+1][c2+1] - self.prefix[r1][c2+1] - self.prefix[r2+1][c1] + self.prefix[r1][c1]
12.2 调试打印技巧
在竞赛中快速调试的小技巧:
python复制def print_matrix(matrix):
for row in matrix:
print(' '.join(f'{x:3}' for x in row))
12.3 时间估算方法
对于m=n=100的矩阵:
- 暴力解法:约10^8次操作 → 很可能超时
- 前缀和解法:约10^4次操作 → 安全
快速估算可以帮助选择正确的算法。
