动态规划的“降维”艺术:二维前缀和与哈希表的终极共鸣
哈喽,各位,我是前端L。
在我们的DP(及其衍生技巧)探险中,“前缀和 + 哈希表”这对黄金搭档,已经在一维世界里证明了它们的惊人威力,无论是精确计数(LC 560)、判定存在性(LC 523),还是处理模运算(LC 974),都游刃有余。
今天,我们将把这套强大的组合拳,“升维”到二维矩阵的战场!我们要解决的问题是:在一个二维矩阵中,找出所有“元素和恰好等于目标值 K”的子矩阵,并计算它们的总数量。
这道题,是二维前缀和思想与一维子数组和计数技巧的完美结合,是对我们“问题转化”和“模型复用”能力的终极考验。
力扣 1074. 元素和为目标值的子矩阵数量
https://leetcode.cn/problems/number-of-submatrices-that-sum-to-target/

题目分析: 给定一个二维矩阵 matrix 和一个整数 target,返回子矩阵元素总和等于 target 的数量。
-
子矩阵:由四个坐标
(r1, c1)(左上角) 和(r2, c2)(右下角) 定义的连续矩形区域。
思路一:二维前缀和的“初级应用” (O(m²n²))
我们首先想到,可以用二维前缀和 preSum[i][j] 来快速计算任意子矩阵的和(回顾 LC 304)。 sum(r1, c1, r2, c2) = preSum[r2+1][c2+1] - preSum[r1][c2+1] - preSum[r2+1][c1] + preSum[r1][c1]
有了这个 O(1) 的求和工具,我们就可以暴力枚举所有的子矩阵:
-
用
r1,c1遍历所有可能的左上角。 -
用
r2,c2遍历所有可能的右下角 (r2 >= r1,c2 >= c1)。 -
计算子矩阵和,如果等于
target,计数器加1。
这个方法需要四层循环,时间复杂度是 O(m²n²),对于较大的矩阵来说,依然无法接受。
思路二:“降维打击” + “LC 560 模型复用” (O(m²n) or O(mn²))
O(m²n²) 的瓶颈在于同时枚举了四个坐标。我们能不能固定其中两个,把问题简化?
核心思想:枚举子矩阵的“上下边界”!
-
固定上下边界:我们先确定子矩阵的顶行
r1和底行r2(r1 <= r2)。 -
“压扁”成一维:对于固定的
r1和r2,我们可以计算出一个一维数组rowSums,其中rowSums[c]表示在第c列,从r1行到r2行所有元素的和。rowSums[c] = sum(matrix[r][c] for r from r1 to r2) -
“Aha!”时刻:现在,在这个一维数组
rowSums中,寻找有多少个连续子数组rowSums[c1...c2],其和恰好等于target? 这不就是我们已经完美解决的 LC 560. 和为 K 的子数组 吗?!
算法流程:
-
(可选但推荐)预计算二维前缀和
preSum。虽然不是直接用它O(1)查子矩阵,但它可以帮助我们快速计算步骤2中的rowSums数组。rowSums[c] = preSum[r2+1][c+1] - preSum[r1][c+1] - (preSum[r2+1][c] - preSum[r1][c])(这个公式复杂了) 更优的rowSums计算:在r2的循环内部,增量计算rowSums。当r2从r1开始递增时,rowSums数组只需要在前一次r2的基础上,加上当前matrix[r2]这一行的元素即可。这样避免了重复计算。 -
初始化总计数
totalCount = 0。 -
外层循环:遍历所有可能的顶行
r1(从0到m-1)。 -
中层循环:遍历所有可能的底行
r2(从r1到m-1)。 a. 计算/更新一维rowSums数组: 对于当前的r1和r2,计算出rowSums[0...n-1]。 * (推荐做法)如果r2 == r1,rowSums就是matrix[r1]这一行。 * 如果r2 > r1,rowSums可以在上一步r2-1的rowSums基础上,累加上matrix[r2]这一行的值。 b. 内层问题 (调用 LC 560 逻辑):对当前得到的rowSums数组,应用“前缀和 + 哈希表”的技巧,计算出其中有多少个子数组的和等于target。将这个计数累加到totalCount中。 -
所有
(r1, r2)遍历完毕后,totalCount就是最终答案。
代码实现
#include <vector>
#include <unordered_map>class Solution {
public:int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {int m = matrix.size();int n = matrix[0].size();int totalCount = 0;// 外层循环:固定上边界 r1for (int r1 = 0; r1 < m; ++r1) {// rowSums 存储 [r1...r2] 行之间,每一列的和vector<long long> rowSums(n, 0); // 中层循环:固定下边界 r2for (int r2 = r1; r2 < m; ++r2) {// 更新 rowSums 数组(增量计算)for (int c = 0; c < n; ++c) {rowSums[c] += matrix[r2][c];}// --- 内层问题:LC 560 ---// 对 rowSums 数组,应用前缀和+哈希表,计算和为 target 的子数组个数unordered_map<long long, int> preSumFreq;preSumFreq[0] = 1;long long currentSum = 0;for (long long sum : rowSums) {currentSum += sum;long long needed = currentSum - target;if (preSumFreq.count(needed)) {totalCount += preSumFreq[needed];}preSumFreq[currentSum]++;}// --- 内层问题结束 ---}}return totalCount;}
};
(注:使用 long long 防止 rowSums 或 currentSum 溢出)
总结:降维打击,模型复用的力量
今天这道题,是二维问题转化为一维问题的绝佳典范。它深刻地展示了:
面对高维度问题时,尝试通过“固定部分维度”的方式,将其“拍扁”成我们熟悉的低维度问题,是一种极其强大的解题策略。
我们没有直接在二维dp表上硬磕,而是巧妙地:
-
枚举了子矩阵的行范围 (
r1,r2)。 -
将这个范围内的矩阵压缩成了一个一维数组 (
rowSums)。 -
在这个一维数组上,复用了我们已经掌握的“前缀和+哈希表”模型。
这不仅仅是解出了一道题,更是我们算法思维武器库的一次重要升级。掌握了这种“降维 + 模型复用”的打法,你将能应对更多看似无从下手的复杂问题。
咱们下期见~
