哈喽,各位,我是前端L。
在 上一篇文章 中,我们刚刚掌握了一维世界里的"闪电查询"秘籍------前缀和。通过一次 O(n) 的预处理,我们实现了 O(1) 的区间和查询。这个"空间换时间"的策略是如此优雅和强大。
现在,是时候将我们的战场,从一条"线"扩展到一个"面"了!我们将把前缀和的思想,从一维升维 到二维,构建一个能够实现矩阵区域和 O(1) 查询的"超级索引"。这不仅是技巧的升级,更是对"容斥原理"这一深刻数学思想的一次精彩演绎。
力扣 304. 二维区域和检索 - 矩阵不可变
https://leetcode.cn/problems/range-sum-query-2d-immutable/

题目分析: 你需要设计一个数据结构 NumMatrix:
-
NumMatrix(vector<vector<int>>& matrix):用一个二维矩阵matrix初始化。 -
int sumRegion(int row1, int col1, int row2, int col2):返回该矩阵中,由(row1, col1)(左上角)和(row2, col2)(右下角)定义的矩形区域内所有元素的和。
核心约束:
-
matrix不可变。 -
sumRegion会被频繁调用。
和一维情况一样,"频繁调用"就是最强的信号:预处理势在必行!
从一维到二维:前缀和的优雅"升维"
我们已经知道,一维前缀和 preSum[i] 代表 nums[0...i-1] 的和。现在,我们需要一个二维的 preSum 矩阵。
1. DP状态定义 (二维前缀和的核心): preSum[i][j] 表示:原矩阵 matrix 中,以 (0, 0) 为左上角,以 (i-1, j-1) 为右下角的那个矩形区域内所有元素的和。
2. 如何构建 preSum 矩阵?------ 容斥原理的第一次闪耀 这是二维前缀和最精妙的地方。如何递推计算 preSum[i][j]? 想象一下 preSum[i][j] 所代表的那个大矩形。我们可以通过它相邻的、更小的三个前缀和矩形来构建它。
(j-1) (j)
(i-1) +-------+-----+
| A | B | <- preSum[i-1][j] = A+B
+-------+-----+
(i) | C | D | <- matrix[i-1][j-1] = D
+-------+-----+
^
|
preSum[i][j-1] = A+C
我们想求 A+B+C+D。
-
我们可以先加上它上方的矩形
A+B(preSum[i-1][j])。 -
再加上它左方的矩形
A+C(preSum[i][j-1])。 -
这时,
(A+B) + (A+C),我们发现左上角的区域A(preSum[i-1][j-1]) 被重复加了两次!必须减掉一次。 -
最后,别忘了加上当前右下角那个单独的元素
D(matrix[i-1][j-1])。
于是,我们得到了二维前缀和的构建公式: preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] - preSum[i-1][j-1] + matrix[i-1][j-1]
3. 如何使用 preSum 矩阵查询?------ 容斥原理的第二次闪耀 现在,我们要查询以 (r1, c1) 为左上角,(r2, c2) 为右下角的矩形和(我们称之为区域 D)。
(c1) (c2+1)
(r1) +-------+-------+
| A | B |
+-------+-------+
(r2+1)| C | D |
+-------+-------+
-
我们可以先获取覆盖
A+B+C+D的最大前缀和矩形:preSum[r2+1][c2+1]。 -
然后,减去我们不想要的上方矩形
A+B:preSum[r1][c2+1]。 -
再减去我们不想要的左方矩形
A+C:preSum[r2+1][c1]。 -
这时,
(A+B+C+D) - (A+B) - (A+C),我们发现左上角的区域A(preSum[r1][c1]) 被重复减了两次!必须加回来一次。
于是,我们得到了二维区域和查询的 O(1) 公式: sumRegion = preSum[r2+1][c2+1] - preSum[r1][c2+1] - preSum[r2+1][c1] + preSum[r1][c1]
代码实现
class NumMatrix {
private:
// preSum[i][j] 存储 matrix[0...i-1][0...j-1] 的和
vector<vector<long long>> preSum; // 使用long long防止溢出
public:
NumMatrix(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) {
return;
}
int m = matrix.size();
int n = matrix[0].size();
// 初始化 preSum 矩阵,大小 (m+1)x(n+1)
preSum.resize(m + 1, vector<long long>(n + 1, 0));
// --- O(m*n) 预处理 ---
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
preSum[i][j] = preSum[i - 1][j]
+ preSum[i][j - 1]
- preSum[i - 1][j - 1]
+ matrix[i - 1][j - 1];
}
}
}
int sumRegion(int row1, int col1, int row2, int col2) {
// --- O(1) 查询 ---
return preSum[row2 + 1][col2 + 1]
- preSum[row1][col2 + 1]
- preSum[row2 + 1][col1]
+ preSum[row1][col1];
}
};
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix* obj = new NumMatrix(matrix);
* int param_1 = obj->sumRegion(row1,col1,row2,col2);
*/
总结:二维前缀和------预处理的"杀手锏"
今天,我们成功地将一维前缀和的智慧,"升维"到了二维世界。二维前缀和技术,是处理静态二维矩阵区间查询问题的"标准答案"和"杀手锏"。
其核心,在于两次巧妙运用容斥原理 (Inclusion-Exclusion Principle):
-
构建时 :
当前 = 上 + 左 - 左上 + 自己 -
查询时 :
目标 = 右下 - 上 - 左 + 左上
掌握了二维前缀和,你就拥有了在二维平面上进行 O(1) 区域求和的"超能力"。这不仅在算法竞赛中极其有用,在实际的图像处理、数据分析等工程领域,也同样是构建高效系统的基石。
咱们下期见!