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 从二维到一维的转换
为了优化算法,我们需要将二维问题转化为一维问题。核心思路是固定矩阵的上下边界,将每一列的和看作一个一维数组的元素。
具体步骤:
- 枚举所有可能的行范围(top, bottom)
- 对于每个行范围,计算每一列的和,形成一个一维数组
- 在这个一维数组中寻找不超过k的最大子数组和
3.2 一维问题的解法
对于一维数组,我们需要找到子数组和不超过k的最大值。这可以通过前缀和+有序集合的方式高效解决:
- 计算一维数组的前缀和数组S
- 对于每个j,我们需要找到i使得S[j] - S[i] ≤ k
- 这等价于找到i使得S[i] ≥ S[j] - k
- 使用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 行列数差异的处理
当矩阵的行数远大于列数(或反之)时,我们可以调整枚举策略以获得更好的性能。具体来说:
- 如果行数m > 列数n,我们改为枚举左右边界,然后对每一行进行压缩
- 这样可以减少外层循环的次数,从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 时间复杂度
基础二维前缀和+TreeSet解法:
- 预处理:O(mn)
- 枚举上下边界:O(m²)
- 对每个边界组合处理列:O(n log n)
- 总复杂度:O(m² n log n)
行列优化后的解法:
- 外层循环: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 空间复杂度
- 基础解法:O(mn)用于存储前缀和数组
- 优化解法:O(max(m,n))用于存储压缩数组和TreeSet
7. 实际应用与注意事项
7.1 边界条件处理
在实际编码中需要注意几个关键点:
- TreeSet初始化时要加入0,对应空子矩阵的情况
- 矩阵元素可能为负数,因此不能使用滑动窗口等基于单调性的优化
- 结果初始值应设为Integer.MIN_VALUE,因为可能所有子矩阵和都大于k
7.2 性能优化技巧
- 对于特别大的k,可以提前检查整个矩阵的和
- 当找到等于k的解时可以立即返回,这是最优解
- 在TreeSet操作前可以先检查是否有可能的改进,避免不必要的操作
7.3 常见错误
- 忘记处理空子矩阵的情况(前缀和为0)
- 错误计算子矩阵边界导致数组越界
- 在行列优化版本中混淆行和列的处理顺序
8. 扩展思考
这个问题可以延伸到多个方向:
- 如果要求恰好等于k的最大子矩阵和该如何修改?
- 如果矩阵可以动态更新,如何设计数据结构支持快速查询?
- 在分布式环境下,如何分割矩阵以并行计算?
在实际工程中,类似的技术可以应用于:
- 图像处理中的区域特征提取
- 金融数据分析中的最大收益区域查找
- 地理信息系统中的热点区域分析