LeetCode 304. 二维区域和检索 - 矩阵不可变
题目描述



题意分析
矩阵初始化后不再修改,需要多次查询由左上角
(row1, col1)和右下角(row2, col2)围成的矩形元素和,两个边界都包含在内。每次重新遍历查询区域会重复计算大量重叠部分。由于矩阵固定,可以先保存从左上角出发的所有前缀矩形和,再用固定次数的加减得到任意查询区域。
解法:二维前缀和
核心思路
[!blue]
定义
prefix[i][j]为原矩阵前i行、前j列的元素和,也就是行范围[0, i)、列范围[0, j)。因此表的大小是(m + 1) × (n + 1),第0行或第0列表示空矩形,值都为0。建表时,把上方前缀
prefix[i - 1][j]与左方前缀prefix[i][j - 1]相加,左上角prefix[i - 1][j - 1]被计算了两次,需要减去一次,最后再加入两者都没有包含的当前格matrix[i - 1][j - 1]。按行列递增计算时,这三个依赖都已经就绪。查询也使用同样的容斥。先取覆盖到右下角的
prefix[row2 + 1][col2 + 1],减去目标上方的prefix[row1][col2 + 1],再减去左方的prefix[row2 + 1][col1]。这时左上重叠区域被多减了一次,所以要加回prefix[row1][col1]。零行、零列把贴着矩阵边界的查询统一成同一个公式:目标上方或左方不存在区域时,对应前缀值自然为零,无需额外分支。
解题步骤
- 构造时创建
m + 1行、n + 1列的前缀和表,保留第0行与第0列为零。- 遍历
i = 1..m、j = 1..n,按照“上方 + 左方 - 左上重叠 + 当前格”计算prefix[i][j]。- 每次查询把右下角的行、列下标分别加一,得到包含该边界的前缀矩形。
- 返回“大矩形 - 上方 - 左方 + 左上重叠”,只读取四个前缀值。
代码实现
class NumMatrix {
private final 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];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
// 闭区间右下角加一,最后补回重复扣除的左上角
return prefix[row2 + 1][col2 + 1]
- prefix[row1][col2 + 1]
- prefix[row2 + 1][col1]
+ prefix[row1][col1];
}
}
type NumMatrix struct {
prefix [][]int
}
func Constructor(matrix [][]int) NumMatrix {
m, n := len(matrix), len(matrix[0])
// 前缀下标表示行列数量,额外零边界对应空矩形
prefix := make([][]int, m+1)
for i := range prefix {
prefix[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for 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]
}
}
return NumMatrix{prefix: prefix}
}
func (m *NumMatrix) SumRegion(row1 int, col1 int, row2 int, col2 int) int {
// 闭区间右下角加一,最后补回重复扣除的左上角
return m.prefix[row2+1][col2+1] -
m.prefix[row1][col2+1] -
m.prefix[row2+1][col1] +
m.prefix[row1][col1]
}
复杂度分析
- 时间复杂度:构造 $O(mn)$,每次查询 $O(1)$。
- 空间复杂度:$O(mn)$,保存二维前缀和。
关键点总结
[!green]
- 前缀表的下标表示行列数量,原矩阵的下标表示格子位置,两者相差一。
- 建表时重叠区域被多加,查询时重叠区域被多减,所以修正符号不同。
- 一次预处理承担全部遍历成本,矩阵保持不变后,每次查询都是常数时间。
易错点总结
[!yellow]
- 查询是闭区间,右下角必须加一;左上角用于表示要排除的行列数量,不加一。
- 查询时漏掉最后加回的左上前缀,会把重叠部分多减一次。
- 建表时当前格是
matrix[i - 1][j - 1],不能直接使用前缀表的下标访问原矩阵。- Java 代码沿用标准
int返回接口,适用于查询结果能由int表示的情况;若需支持更宽的结果,必须连同前缀表和返回类型一起扩展,仅扩大中间变量不能解决返回范围限制。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 303. 区域和检索 - 数组不可变 | 简单 | 把一维前缀差推广到二维矩形,需要四个角做容斥。 |
| 307. 区域和检索 - 数组可修改 | 中等 | 原题存在更新操作,静态前缀和不再适合每次重建,需要树状数组等动态结构。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!