1. 问题背景与核心挑战
今天要拆解的是LeetCode第1074题"元素和为目标值的子矩阵数量"。这道题在字节跳动、腾讯等大厂面试中频繁出现,考察的是对二维前缀和与哈希表结合运用的理解。题目要求我们找出所有满足子矩阵元素和等于目标值target的子矩阵数量。
给定一个m x n的整数矩阵matrix和一个整数target,我们需要返回元素总和等于target的非空子矩阵的数量。子矩阵是矩阵中连续的行和列组成的矩形区域。
提示:这道题的暴力解法时间复杂度高达O(n^4),面试官期待的是O(n^3)的优化解法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析与优化路径
2.1 暴力解法及其局限性
最直观的解法是枚举所有可能的子矩阵,计算它们的和并与target比较。对于一个m x n的矩阵,子矩阵的数量是O(m²n²),每个子矩阵求和需要O(mn)时间,总时间复杂度达到O(m³n³),这在m和n较大时完全不可行。
java复制// 暴力解法伪代码(仅用于理解问题,实际不可行)
int count = 0;
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 sum = 0;
for (int i = i1; i <= i2; i++) {
for (int j = j1; j <= j2; j++) {
sum += matrix[i][j];
}
}
if (sum == target) count++;
}
}
}
}
return count;
2.2 二维前缀和优化
我们可以使用二维前缀和将子矩阵求和的时间复杂度降到O(1)。定义prefix[i][j]表示从(0,0)到(i-1,j-1)的子矩阵和,则有:
code复制prefix[i][j] = matrix[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]
任意子矩阵(i1,j1)到(i2,j2)的和可以通过前缀和数组快速计算:
code复制sum = prefix[i2+1][j2+1] - prefix[i1][j2+1] - prefix[i2+1][j1] + prefix[i1][j1]
这样优化后,时间复杂度降为O(m²n²),但对于较大的矩阵仍然不够高效。
2.3 哈希表优化思路
借鉴LeetCode 560题"和为K的子数组"的思路,我们可以将二维问题转化为一维问题处理:
- 枚举所有可能的行对(上边界和下边界)
- 对于每个行对,将列压缩成一维数组
- 在这个一维数组上使用哈希表统计和为target的子数组
这种方法将时间复杂度优化到O(m²n),是面试官期望的标准解法。
3. 完整代码实现与逐行解析
java复制public int numSubmatrixSumTarget(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int[][] prefix = new int[m + 1][n + 1];
// 计算二维前缀和
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
prefix[i][j] = matrix[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1];
}
}
int count = 0;
// 枚举所有行对(上边界和下边界)
for (int top = 1; top <= m; top++) {
for (int bottom = top; bottom <= m; bottom++) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // 初始状态,和为0出现1次
// 遍历每一列
for (int col = 1; col <= n; col++) {
// 计算当前列的前缀和
int currSum = prefix[bottom][col] - prefix[top-1][col];
// 查找是否有preSum满足currSum - preSum = target
count += map.getOrDefault(currSum - target, 0);
// 更新当前和的出现次数
map.put(currSum, map.getOrDefault(currSum, 0) + 1);
}
}
}
return count;
}
3.1 关键代码解析
-
前缀和计算:
- 我们创建了一个(m+1)x(n+1)的前缀和数组,多出的1行1列用于处理边界情况
- 计算时注意matrix的索引比prefix小1
-
哈希表优化:
- 对于每个行对(top, bottom),我们相当于固定了行的范围
- 然后从左到右遍历列,计算当前列的前缀和currSum
- 使用哈希表记录各个和出现的次数,查找currSum - target是否出现过
-
初始状态处理:
map.put(0, 1)非常重要,这表示和为0的子矩阵(空矩阵)出现过1次- 这样当currSum == target时,可以正确计数
4. 复杂度分析与边界情况
4.1 时间复杂度
- 前缀和计算:O(mn)
- 双重循环枚举行对:O(m²)
- 内部列遍历和哈希表操作:O(n)
- 总时间复杂度:O(mn + m²n) = O(m²n) (当m ≤ n时)
4.2 空间复杂度
- 前缀和数组:O(mn)
- 哈希表:O(n) (每次处理行对时使用)
- 总空间复杂度:O(mn)
4.3 边界情况处理
- 空矩阵:题目要求非空子矩阵,所以不需要特殊处理
- 单元素矩阵:当m=n=1时,直接比较该元素与target
- 负数和零:算法对负数和零同样适用
- 大数溢出:题目限制数值范围在[-1000,1000],所以不会溢出
5. 常见错误与调试技巧
5.1 典型错误案例
-
前缀和索引错误:
java复制// 错误写法:直接使用matrix的尺寸 int[][] prefix = new int[m][n]; // 正确应该是 int[][] prefix = new int[m+1][n+1]; -
哈希表初始状态遗漏:
java复制// 忘记初始化会导致漏计数 Map<Integer, Integer> map = new HashMap<>(); // 必须添加 map.put(0, 1); -
行列遍历顺序错误:
java复制// 错误的遍历顺序(先列后行) for (int col = 1; col <= n; col++) { for (int top = 1; top <= m; top++) { // ... } }
5.2 调试技巧
-
小矩阵测试:
- 从1x1、2x2矩阵开始验证
- 例如matrix=[[1]], target=1应该返回1
-
打印中间结果:
java复制System.out.println("Prefix matrix:"); for (int[] row : prefix) { System.out.println(Arrays.toString(row)); } -
可视化思考:
- 画出行列示意图,标注前缀和计算过程
- 对于特定子矩阵,手动计算验证
6. 变种问题与扩展思考
6.1 相关变种题目
-
最大子矩阵和:
- LeetCode 面试题17.24
- 使用类似前缀和+Kadane算法
-
矩形区域不超过K的最大数值和:
- LeetCode 363
- 需要结合TreeSet进行优化
-
全1子矩阵数量:
- LeetCode 1504
- 使用单调栈优化
6.2 算法扩展应用
-
图像处理:
- 计算图像特定区域的特征值
- 用于卷积核运算优化
-
数据统计分析:
- 快速统计二维数据区域的聚合值
- 在OLAP系统中广泛应用
-
游戏开发:
- 地图区域属性计算
- 碰撞检测优化
7. 面试技巧与实战建议
-
解题步骤建议:
- 先阐述暴力解法,分析复杂度
- 引入前缀和优化
- 进一步优化到O(m²n)解法
- 最后讨论边界情况和优化空间
-
代码书写规范:
- 使用有意义的变量名(top/bottom而不是i/j)
- 添加关键注释
- 保持一致的缩进风格
-
沟通技巧:
- 明确说明每个优化步骤的思考过程
- 主动讨论时间/空间复杂度的权衡
- 询问面试官是否需要处理特定边界情况
在实际面试中,这道题通常作为中等偏难题出现,完整写出优化解法并正确分析复杂度,通常就能获得不错的评价。建议在理解的基础上记忆核心模板,因为类似的二维前缀和+哈希表优化思路可以解决一系列相关问题。
