三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

LeetCode 3070:二维前缀和与滑动窗口优化子矩阵统计

LeetCode 3070:二维前缀和与滑动窗口优化子矩阵统计

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 基础实现步骤

  1. 构建m+1 x n+1的前缀和矩阵
  2. 四重循环枚举所有可能的子矩阵
  3. 使用前缀和快速计算子矩阵和
  4. 统计满足条件的子矩阵数量

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 + 1

4.2 扩展到二维情况

将每列的和压缩成一维数组后,可以应用滑动窗口技巧:

  1. 固定上下边界i1和i2
  2. 计算每列的和形成一维数组
  3. 在该数组上使用滑动窗口统计

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 res

6. 复杂度分析与对比

方法时间复杂度空间复杂度适用场景
暴力解法O(m²n²)O(1)小矩阵(m,n<50)
二维前缀和O(m²n²)O(mn)需要多次查询
列前缀和+滑动窗口O(m²n)O(n)大矩阵优化

7. 边界条件与测试用例

7.1 常见边界情况

  1. 空矩阵输入
  2. k为负数
  3. 矩阵元素全为正/负
  4. 单行/单列矩阵

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 输出:16

8. 实际应用场景

  1. 图像处理:统计特定亮度区域的分布
  2. 数据分析:查找满足条件的子数据集
  3. 金融分析:识别特定波动范围的区域
  4. 游戏开发:地图区域属性统计

9. 算法扩展与变种

  1. 改为统计元素和等于k的子矩阵
  2. 查找最大子矩阵和不超过k
  3. 改为三维前缀和应用
  4. 带权重的前缀和计算

10. 常见错误与调试技巧

  1. 前缀和数组下标越界:通常需要(m+1)x(n+1)的数组
  2. 滑动窗口移动条件错误:注意是while不是if
  3. 初始化错误:前缀和数组首行首列应初始化为0
  4. 整数溢出:对大数使用long类型

调试建议:先在小矩阵上手动计算验证前缀和是否正确

11. 性能优化实践

  1. 提前终止:当最小元素都>k时可提前结束
  2. 并行计算:不同行区间可以并行处理
  3. 内存优化:滚动数组减少空间使用
  4. 预处理:对全正数矩阵可额外优化

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]; // 注意数组初始化为0

12.3 Go实现

preSum := make([][]int, m+1) for i := range preSum { preSum[i] = make([]int, n+1) }

13. 可视化理解技巧

  1. 画图标记前缀和计算过程
  2. 用颜色标注不同子矩阵范围
  3. 制作滑动窗口移动动画
  4. 对比暴力法和优化法的计算量差异

14. 学习资源推荐

  1. 《算法导论》分治算法章节
  2. LeetCode前缀和相关题目
  3. 动态规划与预处理技巧
  4. 滑动窗口算法专题

15. 面试考察要点

  1. 能否从暴力法想到优化思路
  2. 二维前缀和的推导能力
  3. 滑动窗口的应用灵活性
  4. 边界条件的处理完整性
  5. 复杂度分析的准确性

16. 个人解题心得

在实际编码时,我发现以下几点特别重要:

  1. 前缀和数组的大小要比原矩阵大1
  2. 子矩阵坐标转换容易出错,建议画图辅助
  3. 滑动窗口的移动条件要仔细推敲
  4. 对于大矩阵,优化版的性能提升非常明显

建议先从小的测试用例开始,逐步验证每个步骤的正确性,再扩展到一般情况。这类二维前缀和问题有固定模式,掌握后可以解决一系列类似问题。

← 返回列表