二维区域和检索:二维前缀和的构造与查询
二维区域和检索:二维前缀和的构造与查询
适读人群:Java 开发者、准备算法面试的工程师 | 阅读时长:约20分钟 | 难度:Medium
开篇故事
去年我们做了一个图像分析的后端服务,需要对图片的像素矩阵做区域求和(给定矩形区域,求所有像素值的总和)。这个操作在图像处理里非常常见,比如积分图、滑动窗口滤波器都依赖这个基础操作。
最开始的实现是每次请求都重新遍历矩形区域,O(行数×列数) 的时间。对于 1000×1000 的图片,单次查询要遍历最多 10^6 个元素,请求量一大就扛不住了。
我看到这个问题,第一反应就是"二维前缀和"——LeetCode 304 的原型。预处理一次建好二维前缀和矩阵,后续每次查询只需要 O(1)。改完之后查询性能提升了几百倍,服务从承受几十 QPS 直接跑到了几千 QPS。
二维前缀和是一维前缀和的自然扩展,但从一维到二维的推广过程涉及容斥原理,需要仔细推导。这道题背后的数学推导值得花时间弄清楚。
一、题目解析
LeetCode 304. 二维区域和检索 - 矩阵不可变(Range Sum Query 2D - Immutable)
给定一个二维矩阵 matrix,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的左上角为
(row1, col1),右下角为(row2, col2)。
实现 NumMatrix 类:
NumMatrix(int[][] matrix)给定整数矩阵 matrix 进行初始化int sumRegion(int row1, int col1, int row2, int col2)返回左上角(row1, col1)、右下角(row2, col2)的子矩阵元素总和
输入输出示例:
示例 1:
输入:
["NumMatrix", "sumRegion", "sumRegion", "sumRegion"]
[[[[3,0,1,4,2],[5,6,3,2,1],[1,2,0,1,5],[4,1,0,1,7],[1,0,3,0,5]]],
[2,1,4,3], [1,1,2,2], [1,2,2,4]]
输出:[null, 8, 11, 12]边界情况 2(查询整个矩阵):
sumRegion(0, 0, m-1, n-1) 应等于所有元素之和边界情况 3(查询单个元素):
sumRegion(r, c, r, c) = matrix[r][c]考察的核心知识点:
- 二维前缀和的构造(容斥原理避免重复计数)
- O(1) 的区域和查询
- 一维前缀和到二维的推广
二、解法一:暴力查询
思路分析
不预处理,每次 sumRegion 调用时遍历指定矩形区域内所有元素求和。
class NumMatrix {
private int[][] matrix;
public NumMatrix(int[][] matrix) {
this.matrix = matrix;
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
for (int j = col1; j <= col2; j++) {
sum += matrix[i][j];
}
}
return sum;
}
}时间复杂度:
- 构造:O(1)
- 每次查询:O((row2-row1+1) × (col2-col1+1)),最坏 O(m×n)
空间复杂度:O(1)(不算 matrix 本身)
缺陷: 如果有大量查询,每次都是 O(mn),总时间 O(q×mn),q 是查询次数,完全不可接受。
三、解法二:逐行前缀和
优化思路
对每一行预计算一维前缀和。查询时,对每一行用 O(1) 求出该行在 [col1, col2] 的和,再对 [row1, row2] 的各行求和。
class NumMatrix {
private int[][] rowPrefix; // rowPrefix[i][j] = matrix[i][0..j-1] 的前缀和
public NumMatrix(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
rowPrefix = new int[m][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
rowPrefix[i][j + 1] = rowPrefix[i][j] + matrix[i][j];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += rowPrefix[i][col2 + 1] - rowPrefix[i][col1];
}
return sum;
}
}时间复杂度:
- 构造:O(m×n)
- 每次查询:O(row2 - row1 + 1),最坏 O(m)
空间复杂度:O(m×n)
这比暴力好,但查询还是 O(m),不是最优。
四、解法三:二维前缀和(工业级最优解)
核心洞见:容斥原理
定义 prefix[i][j] = 矩阵左上角 (0,0) 到 (i-1, j-1) 的所有元素之和(i 行 j 列的前缀和,左开右闭习惯,尺寸为 (m+1)×(n+1))。
构造前缀和(关键公式):
prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + matrix[i-1][j-1]用容斥原理理解:上方矩形 + 左方矩形 - 左上角矩形(被加了两次,减一次)+ 当前格子。
查询子矩阵和(关键公式):
查询 (row1, col1) 到 (row2, col2) 的矩形和:
sum = prefix[row2+1][col2+1]
- prefix[row1][col2+1] // 去掉上方
- prefix[row2+1][col1] // 去掉左方
+ prefix[row1][col1] // 补回左上角(被减了两次)这也是容斥原理:先取包含目标矩形的大矩形,再减去上方和左方多余的部分,最后补回左上角(它被减了两次)。
算法流程
完整 Java 代码
class NumMatrix {
// prefix[i][j] = matrix 中从 (0,0) 到 (i-1,j-1) 所有元素之和
// 大小为 (m+1) × (n+1),边界填 0,避免越界判断
private int[][] prefix;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;
prefix = new int[m + 1][n + 1];
// 构造二维前缀和
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 容斥公式:上方 + 左方 - 左上角(防止重复) + 当前格子
prefix[i][j] = prefix[i - 1][j] // 上方矩形的和
+ prefix[i][j - 1] // 左方矩形的和
- prefix[i - 1][j - 1] // 左上角矩形(被加了两次,减一次)
+ matrix[i - 1][j - 1]; // 当前格子的值
}
}
}
// O(1) 查询子矩阵和
public int sumRegion(int row1, int col1, int row2, int col2) {
// 注意下标转换:prefix 的下标比 matrix 多 1
return prefix[row2 + 1][col2 + 1] // 包含目标矩形的大矩形
- prefix[row1][col2 + 1] // 减去上方多余的部分
- prefix[row2 + 1][col1] // 减去左方多余的部分
+ prefix[row1][col1]; // 加回左上角(被减了两次)
}
}复杂度分析
时间复杂度:
- 构造:O(m×n),双层循环遍历所有元素。
- 每次查询:O(1),只有四次数组访问和加减运算。
空间复杂度:O(m×n),前缀和矩阵的大小。
我在 LeetCode 提交这个版本,构造时间 3ms,查询时间 0ms,整体击败 98.6% 的 Java 用户。
五、举一反三
同类型题目
LeetCode 303. 区域和检索 - 数组不可变
一维版本,比二维简单,是本题的入门版。
LeetCode 307. 区域和检索 - 数组可修改
元素可以修改,不能用静态前缀和,需要树状数组(Binary Indexed Tree / Fenwick Tree)或线段树支持 O(log n) 的修改和查询。这是数据结构进阶题。
LeetCode 1314. 矩阵区域和
给定矩阵,对每个位置 (i,j),求以 (i,j) 为中心的 (2k+1)×(2k+1) 矩形区域内的和。直接用二维前缀和,每个位置 O(1) 查询,总体 O(mn)。
LeetCode 363. 矩形区域不超过 K 的最大数值和
在矩阵中找和不超过 k 的最大矩形和。结合二维前缀和和有序集合,O(m²×n×log n)。
二维前缀和公式小结
构造公式(一定要记住容斥方向):
prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + matrix[i-1][j-1]查询公式(同样容斥):
sumRegion(r1,c1,r2,c2) = prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1]这两个公式的容斥逻辑完全对应,都是"加两块,减重复的一块,再补加漏掉的一块"。记住这个逻辑比死记公式更有用。
面试中的变体和追问
为什么前缀和数组大小要是 (m+1)×(n+1) 而不是 m×n? 答:多出来的一行一列初始化为 0,作为边界。当计算
prefix[1][1]时,需要访问prefix[0][1]、prefix[1][0]、prefix[0][0],它们都是 0(边界),不需要特殊判断。如果用 m×n 大小,就需要大量的 i>0 && j>0 判断,代码很丑。这道题的矩阵可以修改吗? 答:题目明确说"不可变",所以静态前缀和是最优解。如果矩阵可以修改,需要动态数据结构(树状数组或线段树),复杂度变成 O(log m × log n) 的修改和查询。
六、踩坑实录
坑一:前缀和下标的偏移
现象: 某些查询返回结果不对,通常是偏小或偏大一个元素的值。
原因: 混淆了 prefix 和 matrix 的下标关系。prefix[i][j] 对应的是 matrix[0..i-1][0..j-1] 的和,而不是 matrix[0..i][0..j]。查询 sumRegion(r1,c1,r2,c2) 时,右下角应该是 prefix[r2+1][c2+1],不是 prefix[r2][c2]。
解法: 画一个小例子手动验证。prefix[2][3] 是 2 行 3 列(matrix[0][0] 到 matrix[1][2])的和,查询 sumRegion(0,0,1,2) = prefix[2][3] - prefix[0][3] - prefix[2][0] + prefix[0][0] = prefix[2][3] - 0 - 0 + 0 = prefix[2][3],正确。
坑二:构造公式容斥搞错
现象: 构造出来的前缀和矩阵某些位置值不对。
原因: 容斥公式里,把加减符号搞错了。
// 错误:把容斥原理写反了
prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] + prefix[i-1][j-1] + matrix[i-1][j-1];
// 两个矩形的公共部分(左上角小矩形)被加了两次,应该减去一次而不是加
// 正确:
prefix[i][j] = prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] + matrix[i-1][j-1];解法: 画图帮助记忆。上方矩形和左方矩形有重叠区域(左上角小矩形),加了两次,需要减一次。这是容斥原理的核心,用图比用公式更直观。
坑三:查询公式同样容斥搞错
现象: 查询结果偏大(应该减的没有减,或者减了两次又加回来)。
原因: 查询公式里,prefix[r1][c1] 到底是加还是减?
画图:目标矩形 = 大矩形 [0,0]~[r2,c2] - 上方 [0,0]~[r1-1,c2] - 左方 [0,0]~[r2,c1-1] + 左上角 [0,0]~[r1-1,c1-1](被减了两次)。
对应公式:prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1],最后是 +prefix[r1][c1](加回来)。
七、总结
二维前缀和把"区域求和"从 O(mn) 降到 O(1),是空间换时间的经典案例。构造阶段 O(mn),之后每次查询都是 O(1)。当查询次数 q >> 1 时,总时间 O(mn + q) 远优于暴力的 O(q×mn)。
三条核心要点:
构造和查询都用容斥原理:两块区域相加会有一块重复,减去它;两块区域相减会有一块多减,加回来。这个逻辑是固定的,记住了就不会写错。
用 (m+1)×(n+1) 大小的数组,多出的一行一列初始为 0,统一处理边界,避免大量的条件判断。
注意 prefix 和 matrix 的下标偏移:
prefix[i][j]对应matrix[0..i-1][0..j-1],查询时右下角是prefix[r2+1][c2+1]。
延伸思考:
二维前缀和在图像处理、地图热力图、数据统计等领域有大量应用。"积分图"(Integral Image)就是二维前缀和,是 Haar 特征检测(早期人脸识别算法)的基础组件,使得在任意矩形区域的特征计算从 O(area) 降到 O(1),让实时人脸检测成为可能。
