问题描述
给你一个正整数 n,表示最初有一个 n x n 的矩阵,每个元素都是 0。
给你一个二维整数数组 queries,其中 queries[i] = [row1, col1, row2, col2]。对于每个查询 i,需要找到所有满足 row1 <= x <= row2 且 col1 <= y <= col2 的位置 (x, y),并将这个位置上的元素加 1。
返回执行完所有查询后得到的矩阵。
暴力解法的问题
如果直接模拟,对每个查询都遍历子矩阵中的每个元素进行加1:
go
for _, q := range queries {
for i := q[0]; i <= q[2]; i++ {
for j := q[1]; j <= q[3]; j++ {
matrix[i][j]++
}
}
}
时间复杂度为 O(queries * n^2),在最坏情况下约为 500^2 * 10^4 = 2.5e9 操作,必然超时。
差分数组的第一性原理
一维差分数组的本质
假设有一个数组 a = [3, 5, 2, 6],我们构造它的差分数组 diff:
text
a = [3, 5, 2, 6]
diff = [3, 2, -3, 4]
其中 diff[i] = a[i] - a[i-1](假设 a[-1] = 0):
diff[0] = 3 - 0 = 3diff[1] = 5 - 3 = 2diff[2] = 2 - 5 = -3diff[3] = 6 - 2 = 4
关键观察:原数组可以通过差分数组的前缀和还原:
text
a[0] = diff[0] = 3
a[1] = diff[0] + diff[1] = 3 + 2 = 5
a[2] = diff[0] + diff[1] + diff[2] = 3 + 2 + (-3) = 2
a[3] = diff[0] + diff[1] + diff[2] + diff[3] = 3 + 2 + (-3) + 4 = 6
即 a[i] = sum(diff[0..i])。
为什么区间更新只需要修改两个端点?
假设要对区间 [l, r] 的所有元素加 val,直接修改需要 O(r-l+1) 次操作。
但在差分数组中,我们只需:
diff[l] += valdiff[r+1] -= val
为什么这样有效?
因为差分数组的前缀和还原性质:
- 对于
i < l:前缀和不受影响,a[i]不变 - 对于
l <= i <= r:前缀和会累加diff[l]的增量val,所以a[i] += val - 对于
i > r:前缀和累加了diff[l]的+val和diff[r+1]的-val,两者抵消,a[i]不变
这就是差分数组的精髓:通过标记边界,利用前缀和的累积效应,实现区间更新。
二维差分数组的推导
二维情况下,我们要对子矩阵 (r1,c1) 到 (r2,c2) 的所有元素加 val。
类比一维,我们需要在差分矩阵 diff 中标记边界,使得二维前缀和还原时,恰好只有目标子矩阵的元素被加上 val。
二维前缀和公式:
text
result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]
要让 (r1,c1) 到 (r2,c2) 区域加 val,我们需要:
- 在
diff[r1][c1]加val:从(r1,c1)开始,右下方所有位置都会累加这个值 - 在
diff[r1][c2+1]减val:阻止c2列右侧继续累加 - 在
diff[r2+1][c1]减val:阻止r2行下方继续累加 - 在
diff[r2+1][c2+1]加val:补偿右下角被重复减去的值
这四个操作确保只有目标矩形区域受到影响。
为什么是这四个位置?
让我们用容斥原理理解:
diff[r1][c1] += val:影响范围是从(r1,c1)到(n-1,n-1)的整个右下区域diff[r1][c2+1] -= val:减去从(r1,c2+1)到右下的区域,排除右侧超出部分diff[r2+1][c1] -= val:减去从(r2+1,c1)到右下的区域,排除下方超出部分diff[r2+1][c2+1] += val:加回(r2+1,c2+1)到右下的区域,因为它被减了两次
通过容斥,最终只有 (r1,c1) 到 (r2,c2) 的矩形区域受到 +val 的影响。
完整示例演示
假设 n=3,queries = [[0,0,1,1]],即对子矩阵 (0,0) 到 (1,1) 加1。
步骤1:初始化差分数组
text
diff:
0 0 0
0 0 0
0 0 0
步骤2:处理查询 [0,0,1,1]
按照四个操作:
diff[0][0] += 1
text
1 0 0
0 0 0
0 0 0
diff[0][2] -= 1(c2+1=2)
text
1 0 -1
0 0 0
0 0 0
diff[2][0] -= 1(r2+1=2)
text
1 0 -1
0 0 0
-1 0 0
diff[2][2] += 1(r2+1=2, c2+1=2)
text
1 0 -1
0 0 0
-1 0 1
步骤3:二维前缀和还原
逐个计算 result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]:
result[0][0] = 0 + 0 - 0 + 1 = 1result[0][1] = 0 + 1 - 0 + 0 = 1result[0][2] = 0 + 1 - 0 + (-1) = 0result[1][0] = 1 + 0 - 0 + 0 = 1result[1][1] = 1 + 1 - 1 + 0 = 1result[1][2] = 1 + 1 - 1 + 0 = 0result[2][0] = 1 + 0 - 0 + (-1) = 0result[2][1] = 0 + 1 - 1 + 0 = 0result[2][2] = 0 + 0 - 0 + 1 = 0
最终矩阵
text
1 1 0
1 1 0
0 0 0
完美!只有 (0,0) 到 (1,1) 的子矩阵被加1了。
代码实现
go
func rangeAddQueries(n int, queries [][]int) [][]int {
// 创建差分数组
diff := make([][]int, n+1)
for i := range diff {
diff[i] = make([]int, n+1)
}
// 处理每个查询,在差分数组中标记
for _, q := range queries {
r1, c1, r2, c2 := q[0], q[1], q[2], q[3]
diff[r1][c1]++
if r2+1 < n {
diff[r2+1][c1]--
}
if c2+1 < n {
diff[r1][c2+1]--
}
if r2+1 < n && c2+1 < n {
diff[r2+1][c2+1]++
}
}
// 计算二维前缀和,得到最终矩阵
result := make([][]int, n)
for i := range result {
result[i] = make([]int, n)
}
for i := 0; i < n; i++ {
for j := 0; j < n; j++ {
// result[i][j] = result[i-1][j] + result[i][j-1] - result[i-1][j-1] + diff[i][j]
if i > 0 {
result[i][j] += result[i-1][j]
}
if j > 0 {
result[i][j] += result[i][j-1]
}
if i > 0 && j > 0 {
result[i][j] -= result[i-1][j-1]
}
result[i][j] += diff[i][j]
}
}
return result
}