1. 问题背景与核心需求
这道LeetCode 3070题要求我们统计所有元素和小于等于k的子矩阵数量。给定一个m x n的整数矩阵和一个整数k,需要找出所有满足子矩阵内元素总和≤k的子矩阵个数。这个问题在二维数据处理、图像分析和统计计算等领域有实际应用价值。
关键提示:暴力解法的时间复杂度为O(m²n²),对于较大矩阵会超时,必须使用优化算法。
2. 二维前缀和算法解析
2.1 前缀和概念延伸
一维前缀和数组preSum[i]表示原数组前i个元素的和。扩展到二维情况,preSum[i][j]表示以(0,0)为左上角、(i,j)为右下角的矩形区域的和。
计算二维前缀和的递推公式:
preSum[i][j] = matrix[i-1][j-1] + preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1]2.2 子矩阵求和优化
利用前缀和数组可以在O(1)时间内计算任意子矩阵和:
sum = preSum[x2][y2] - preSum[x1-1][y2] - preSum[x2][y1-1] + preSum[x1-1][y1-1]3. 算法实现与优化
3.1 基础实现步骤
- 构建m+1 x n+1的前缀和矩阵
- 四重循环枚举所有可能的子矩阵
- 使用前缀和快速计算子矩阵和
- 统计满足条件的子矩阵数量
3.2 时间复杂度优化
通过维护列前缀和可以将复杂度降至O(m²n):
for i1 in range(m): col_prefix = [0]*n for i2 in range(i1, m): for j in range(n): col_prefix[j] += matrix[i2][j] # 在一维数组col_prefix上使用滑动窗口4. 滑动窗口技巧应用
4.1 一维数组的滑动窗口
对于一维数组nums,要求子数组和≤k的数量:
res = 0 curr_sum = 0 left = 0 for right in range(len(nums)): curr_sum += nums[right] while curr_sum > k: curr_sum -= nums[left] left += 1 res += right - left + 14.2 扩展到二维情况
将每列的和压缩成一维数组后,可以应用滑动窗口技巧:
- 固定上下边界i1和i2
- 计算每列的和形成一维数组
- 在该数组上使用滑动窗口统计
5. 完整代码实现
def countSubmatrices(matrix, k): m, n = len(matrix), len(matrix[0]) res = 0 # 方法一:二维前缀和 O(m²n²) preSum = [[0]*(n+1) for _ in range(m+1)] for i in range(1, m+1): for j in range(1, n+1): preSum[i][j] = matrix[i-1][j-1] + preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1] for i1 in range(1, m+1): for j1 in range(1, n+1): for i2 in range(i1, m+1): for j2 in range(j1, n+1): total = preSum[i2][j2] - preSum[i1-1][j2] - preSum[i2][j1-1] + preSum[i1-1][j1-1] if total <= k: res += 1 return res # 方法二:优化版 O(m²n) res = 0 for i1 in range(m): col_sum = [0]*n for i2 in range(i1, m): for j in range(n): col_sum[j] += matrix[i2][j] # 滑动窗口 curr_sum = 0 left = 0 for right in range(n): curr_sum += col_sum[right] while curr_sum > k: curr_sum -= col_sum[left] left += 1 res += right - left + 1 return res6. 复杂度分析与对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(m²n²) | O(1) | 小矩阵(m,n<50) |
| 二维前缀和 | O(m²n²) | O(mn) | 需要多次查询 |
| 列前缀和+滑动窗口 | O(m²n) | O(n) | 大矩阵优化 |
7. 边界条件与测试用例
7.1 常见边界情况
- 空矩阵输入
- k为负数
- 矩阵元素全为正/负
- 单行/单列矩阵
7.2 测试用例示例
测试用例1: matrix = [[1,2,3],[4,5,6],[7,8,9]] k = 10 输出:6 测试用例2: matrix = [[1,0,1],[0,1,0],[1,0,1]] k = 5 输出:168. 实际应用场景
- 图像处理:统计特定亮度区域的分布
- 数据分析:查找满足条件的子数据集
- 金融分析:识别特定波动范围的区域
- 游戏开发:地图区域属性统计
9. 算法扩展与变种
- 改为统计元素和等于k的子矩阵
- 查找最大子矩阵和不超过k
- 改为三维前缀和应用
- 带权重的前缀和计算
10. 常见错误与调试技巧
- 前缀和数组下标越界:通常需要(m+1)x(n+1)的数组
- 滑动窗口移动条件错误:注意是while不是if
- 初始化错误:前缀和数组首行首列应初始化为0
- 整数溢出:对大数使用long类型
调试建议:先在小矩阵上手动计算验证前缀和是否正确
11. 性能优化实践
- 提前终止:当最小元素都>k时可提前结束
- 并行计算:不同行区间可以并行处理
- 内存优化:滚动数组减少空间使用
- 预处理:对全正数矩阵可额外优化
12. 不同语言实现要点
12.1 C++实现
vector<vector<int>> preSum(m+1, vector<int>(n+1)); // 注意int溢出问题12.2 Java实现
int[][] preSum = new int[m+1][n+1]; // 注意数组初始化为012.3 Go实现
preSum := make([][]int, m+1) for i := range preSum { preSum[i] = make([]int, n+1) }13. 可视化理解技巧
- 画图标记前缀和计算过程
- 用颜色标注不同子矩阵范围
- 制作滑动窗口移动动画
- 对比暴力法和优化法的计算量差异
14. 学习资源推荐
- 《算法导论》分治算法章节
- LeetCode前缀和相关题目
- 动态规划与预处理技巧
- 滑动窗口算法专题
15. 面试考察要点
- 能否从暴力法想到优化思路
- 二维前缀和的推导能力
- 滑动窗口的应用灵活性
- 边界条件的处理完整性
- 复杂度分析的准确性
16. 个人解题心得
在实际编码时,我发现以下几点特别重要:
- 前缀和数组的大小要比原矩阵大1
- 子矩阵坐标转换容易出错,建议画图辅助
- 滑动窗口的移动条件要仔细推敲
- 对于大矩阵,优化版的性能提升非常明显
建议先从小的测试用例开始,逐步验证每个步骤的正确性,再扩展到一般情况。这类二维前缀和问题有固定模式,掌握后可以解决一系列类似问题。