LeetCode 363题:二维矩阵最大子矩阵和不超过k的优化解法
2026/9/17 7:59:29 网站建设 项目流程

1. 问题重述与核心挑战

LeetCode 363题要求我们解决一个典型的二维矩阵求和问题:给定一个m x n的整数矩阵和一个整数k,找出矩阵中所有可能的矩形区域,使得该区域内数值之和不超过k,同时是所有可能情况中的最大值。

这个问题的难点在于如何高效地处理二维矩阵中的各种子矩阵组合。对于一个m x n的矩阵,理论上存在O(m²n²)个子矩阵,直接暴力枚举所有可能性在m和n较大时(如100x100)会导致计算量达到10^8级别,这在常规时间限制内是无法完成的。

2. 基础解法:二维前缀和

2.1 前缀和数组构建

二维前缀和是解决这类矩阵区域求和问题的经典方法。我们首先构建一个前缀和数组sum,其中sum[i][j]表示从矩阵左上角(0,0)到(i-1,j-1)位置的矩形区域和。

构建公式为:

sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1]

这个公式利用了容斥原理,避免了重复计算。通过预处理,我们可以在O(1)时间内计算任意子矩阵的和。

2.2 朴素枚举法

有了前缀和数组后,最直观的解法是枚举所有可能的子矩阵:

int maxSum = Integer.MIN_VALUE; for (int i1 = 0; i1 < m; i1++) { for (int j1 = 0; j1 < n; j1++) { for (int i2 = i1; i2 < m; i2++) { for (int j2 = j1; j2 < n; j2++) { int current = sum[i2+1][j2+1] - sum[i1][j2+1] - sum[i2+1][j1] + sum[i1][j1]; if (current <= k) { maxSum = Math.max(maxSum, current); } } } } }

这种方法虽然直观,但时间复杂度为O(m²n²),对于100x100的矩阵来说显然不够高效。

3. 优化思路:降维与二分查找

3.1 从二维到一维的转换

为了优化算法,我们需要将二维问题转化为一维问题。核心思路是固定矩阵的上下边界,将每一列的和看作一个一维数组的元素。

具体步骤:

  1. 枚举所有可能的行范围(top, bottom)
  2. 对于每个行范围,计算每一列的和,形成一个一维数组
  3. 在这个一维数组中寻找不超过k的最大子数组和

3.2 一维问题的解法

对于一维数组,我们需要找到子数组和不超过k的最大值。这可以通过前缀和+有序集合的方式高效解决:

  1. 计算一维数组的前缀和数组S
  2. 对于每个j,我们需要找到i使得S[j] - S[i] ≤ k
  3. 这等价于找到i使得S[i] ≥ S[j] - k
  4. 使用TreeSet维护已遍历的前缀和,可以快速查找满足条件的最小S[i]
TreeSet<Integer> set = new TreeSet<>(); set.add(0); // 初始前缀和为0 int currentSum = 0; int maxSum = Integer.MIN_VALUE; for (int num : nums) { currentSum += num; Integer ceil = set.ceiling(currentSum - k); if (ceil != null) { maxSum = Math.max(maxSum, currentSum - ceil); } set.add(currentSum); }

4. 完整优化算法实现

4.1 Java实现

class Solution { public int maxSumSubmatrix(int[][] matrix, int k) { int m = matrix.length, n = matrix[0].length; int[][] sum = new int[m + 1][n + 1]; // 构建前缀和数组 for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1]; } } int ans = Integer.MIN_VALUE; // 枚举上下边界 for (int top = 1; top <= m; top++) { for (int bottom = top; bottom <= m; bottom++) { TreeSet<Integer> treeSet = new TreeSet<>(); treeSet.add(0); // 初始前缀和为0 // 枚举右边界 for (int right = 1; right <= n; right++) { // 计算从top到bottom行,1到right列的区域和 int area = sum[bottom][right] - sum[top-1][right]; // 寻找满足area - x ≤ k的最小x,即x ≥ area - k Integer left = treeSet.ceiling(area - k); if (left != null) { ans = Math.max(ans, area - left); } treeSet.add(area); } } } return ans; } }

4.2 C++实现

class Solution { public: int maxSumSubmatrix(vector<vector<int>>& matrix, int k) { int m = matrix.size(), n = matrix[0].size(); vector<vector<int>> sum(m + 1, vector<int>(n + 1, 0)); // 构建前缀和数组 for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + matrix[i-1][j-1]; } } int ans = INT_MIN; // 枚举上下边界 for (int top = 1; top <= m; top++) { for (int bottom = top; bottom <= m; bottom++) { set<int> s; s.insert(0); // 初始前缀和为0 // 枚举右边界 for (int right = 1; right <= n; right++) { int area = sum[bottom][right] - sum[top-1][right]; auto it = s.lower_bound(area - k); if (it != s.end()) { ans = max(ans, area - *it); } s.insert(area); } } } return ans; } };

5. 进阶优化:行列选择策略

5.1 行列数差异的处理

当矩阵的行数远大于列数(或反之)时,我们可以调整枚举策略以获得更好的性能。具体来说:

  1. 如果行数m > 列数n,我们改为枚举左右边界,然后对每一行进行压缩
  2. 这样可以减少外层循环的次数,从O(m²)降到O(n²)

5.2 优化后的实现

class Solution { public int maxSumSubmatrix(int[][] matrix, int k) { int m = matrix.length, n = matrix[0].length; boolean isRowLarger = m > n; int outerDim = isRowLarger ? n : m; int innerDim = isRowLarger ? m : n; int ans = Integer.MIN_VALUE; // 枚举外层维度(行数多时枚举列,列数多时枚举行) for (int i = 0; i < outerDim; i++) { int[] compressed = new int[innerDim]; for (int j = i; j < outerDim; j++) { // 压缩矩阵 for (int x = 0; x < innerDim; x++) { compressed[x] += isRowLarger ? matrix[x][j] : matrix[j][x]; } // 在一维数组上求解 TreeSet<Integer> set = new TreeSet<>(); set.add(0); int currentSum = 0; for (int num : compressed) { currentSum += num; Integer ceil = set.ceiling(currentSum - k); if (ceil != null) { ans = Math.max(ans, currentSum - ceil); } set.add(currentSum); } } } return ans; } }

6. 复杂度分析

6.1 时间复杂度

  1. 基础二维前缀和+TreeSet解法:

    • 预处理:O(mn)
    • 枚举上下边界:O(m²)
    • 对每个边界组合处理列:O(n log n)
    • 总复杂度:O(m² n log n)
  2. 行列优化后的解法:

    • 外层循环:O(min(m,n)²)
    • 内层处理:O(max(m,n) log max(m,n))
    • 总复杂度:O(min(m,n)² max(m,n) log max(m,n))

6.2 空间复杂度

  1. 基础解法:O(mn)用于存储前缀和数组
  2. 优化解法:O(max(m,n))用于存储压缩数组和TreeSet

7. 实际应用与注意事项

7.1 边界条件处理

在实际编码中需要注意几个关键点:

  1. TreeSet初始化时要加入0,对应空子矩阵的情况
  2. 矩阵元素可能为负数,因此不能使用滑动窗口等基于单调性的优化
  3. 结果初始值应设为Integer.MIN_VALUE,因为可能所有子矩阵和都大于k

7.2 性能优化技巧

  1. 对于特别大的k,可以提前检查整个矩阵的和
  2. 当找到等于k的解时可以立即返回,这是最优解
  3. 在TreeSet操作前可以先检查是否有可能的改进,避免不必要的操作

7.3 常见错误

  1. 忘记处理空子矩阵的情况(前缀和为0)
  2. 错误计算子矩阵边界导致数组越界
  3. 在行列优化版本中混淆行和列的处理顺序

8. 扩展思考

这个问题可以延伸到多个方向:

  1. 如果要求恰好等于k的最大子矩阵和该如何修改?
  2. 如果矩阵可以动态更新,如何设计数据结构支持快速查询?
  3. 在分布式环境下,如何分割矩阵以并行计算?

在实际工程中,类似的技术可以应用于:

  • 图像处理中的区域特征提取
  • 金融数据分析中的最大收益区域查找
  • 地理信息系统中的热点区域分析

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询