前缀和是一种极其优秀的线性结构,也是一种重要的思想,能极大地降低区间查询的时间复杂度。
为了方便,涉及到前缀和的题目,通常使用全局数组(默认初始化为 0 0 0)且数组下标一般从 1 1 1 开始。
1.二维前缀和
问题描述:假设有一个 n × m n \times m n×m 大小的矩阵 a [ ] [ ] a[\ ][\ ] a[ ][ ],再给出 q q q 次询问,每次询问给出 x 1 , y 1 , x 2 , y 2 x_1,\ y_1,\ x_2,\ y_2 x1, y1, x2, y2 四个数,要求输出以 a [ x 1 ] [ y 1 ] a[x_1][y_1] a[x1][y1] 为左上角、 a [ x 2 ] [ y 2 ] a[x_2][y_2] a[x2][y2] 为右下角的子矩阵的所有元素和。
如果使用暴力解法,每次都遍历一遍给出的子矩阵,计算出答案,这样时间复杂度会达到 O ( n ∗ m ∗ q ) O(n*m*q) O(n∗m∗q),极有可能会 TLE。
如果使用二维前缀和来做的话,能将时间复杂度降到 O ( n ∗ m + q ) O(n*m+q) O(n∗m+q),极大地减少了时间。
1.1 求解二维前缀和数组
二维前缀和数组 s u m [ i ] [ j ] sum[i][j] sum[i][j] 就是原数组中以 a [ 1 ] [ 1 ] a[1][1] a[1][1] 为左上角、 a [ i ] [ j ] a[i][j] a[i][j] 为右下角的子矩阵的元素和。
以下图中的 a [ 3 ] [ 3 ] = 13 a[3][3]=13 a[3][3]=13 为例,其前缀和 s u m [ 3 ] [ 3 ] sum[3][3] sum[3][3] 就是黑框框起来的子矩阵元素之和,即以 a [ 1 ] [ 1 ] a[1][1] a[1][1] 为左上角、 a [ 3 ] [ 3 ] a[3][3] a[3][3] 为右下角的子矩阵的元素和。
从递推的角度来看, s u m [ 3 ] [ 3 ] = s u m [ 3 ] [ 2 ] + s u m [ 2 ] [ 3 ] − s u m [ 2 ] [ 2 ] + a [ 3 ] [ 3 ] sum[3][3]=sum[3][2]+sum[2][3]-sum[2][2]+a[3][3] sum[3][3]=sum[3][2]+sum[2][3]−sum[2][2]+a[3][3],
故有: s u m [ i ] [ j ] = s u m [ i ] [ j − 1 ] + s u m [ i − 1 ] [ j ] − s u m [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] sum[i][j]=sum[i][j-1]+sum[i-1][j]-sum[i-1][j-1]+a[i][j] sum[i][j]=sum[i][j−1]+sum[i−1][j]−sum[i−1][j−1]+a[i][j]。
// 求以a[1][1]为左上角、a[i][j]为右下角的子矩阵中的元素和
void init()
{
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + a[i][j];
}
}
}
1.2 区间查询
每次要查询的答案就是下图中的黄色部分。
当给出 x 1 , y 1 , x 2 , y 2 x_1,\ y_1,\ x_2,\ y_2 x1, y1, x2, y2 时,要查询的值即为 s u m [ x 2 ] [ y 2 ] − s u m [ x 2 ] [ y 1 − 1 ] − s u m [ x 1 − 1 ] [ y 2 ] + s u m [ x 1 − 1 ] [ y 1 − 1 ] sum[x_2][y_2]-sum[x_2][y_1-1]-sum[x_1-1][y_2]+sum[x_1-1][y_1-1] sum[x2][y2]−sum[x2][y1−1]−sum[x1−1][y2]+sum[x1−1][y1−1]。
int get(int x1, int y1, int x2, int y2)
{
return sum[x2][y2] - sum[x2][y1 - 1] - sum[x1 - 1][y2] + sum[x1 - 1][y1 - 1];
}
2.二维差分
2.1 定义
假设有原数组 a [ ] [ ] a[\ ][\ ] a[ ][ ],现构造出一个数组 b [ ] [ ] b[\ ][\ ] b[ ][ ],使得 a [ i ] [ j ] a[i][j] a[i][j] 等于 b [ i ] [ j ] b[i][j] b[i][j] 及其左上所有元素的和,那么 b [ ] [ ] b[\ ][\ ] b[ ][ ] 就称为 a [ ] [ ] a[\ ][\ ] a[ ][ ] 的差分, a [ ] [ ] a[\ ][\ ] a[ ][ ] 就称为 b [ ] [ ] b[\ ][\ ] b[ ][ ] 的前缀和。
可以发现,差分与前缀和是逆运算。
2.2 区间修改
由上述定义可知,差分数组 b [ i ] [ j ] b[i][j] b[i][j] 的前缀和就是原数组 a [ i ] [ j ] a[i][j] a[i][j] 的值。
利用差分数组 b [ ] [ ] b[\ ][\ ] b[ ][ ] 可以快速地对原数组 a [ ] [ ] a[\ ][\ ] a[ ][ ] 进行区间修改,时间复杂度为 O ( 1 ) O(1) O(1)。
假如现在要将原数组 a [ ] [ ] a[\ ][\ ] a[ ][ ] 的以 a [ x 1 ] [ y 1 ] a[x_1][y_1] a[x1][y1] 为左上角、 a [ x 2 ] [ y 2 ] a[x_2][y_2] a[x2][y2] 为右下角的矩形区域里的每个数都加上 x x x,代码如下:
// 以a[x1][y1]为左上角、a[x2][y2]为右下角的子矩阵中的所有元素加上x
void add(int x1, int y1, int x2, int y2, int x)
{
b[x1][y1] += x;
b[x2 + 1][y1] -= x;
b[x1][y2 + 1] -= x;
b[x2 + 1][y2 + 1] += x;
}
2.3 初始化
问题:二维差分数组 b [ ] [ ] b[\ ][\ ] b[ ][ ] 是如何构造出来的呢?
二维差分数组 b [ i ] [ j ] b[i][j] b[i][j] 可用如下公式计算:
b [ i ] [ j ] = a [ i ] [ j ] − a [ i ] [ j − 1 ] − a [ i − 1 ] [ j ] + a [ i − 1 ] [ j − 1 ] b[i][j] = a[i][j] - a[i][j-1] - a[i-1][j] + a[i-1][j-1] b[i][j]=a[i][j]−a[i][j−1]−a[i−1][j]+a[i−1][j−1]
事实上,我们不需要过分关注差分数组 b [ ] [ ] b[\ ][\ ] b[ ][ ] 是怎么构造出来的,只需要知道差分与前缀和是互逆运算即可。
一开始,可以把原数组 a [ ] [ ] a[\ ][\ ] a[ ][ ] 想象成全是 0 0 0,此时相应的差分数组 b [ ] [ ] b[\ ][\ ] b[ ][ ] 也全是 0 0 0。
接下来,对原数组 a [ ] [ ] a[\ ][\ ] a[ ][ ] 的初始值可以做如下考虑:
-
a [ 1 ] [ 1 ] a[1][1] a[1][1] 相当于以 a [ 1 ] [ 1 ] a[1][1] a[1][1] 为左上角、 a [ 1 ] [ 1 ] a[1][1] a[1][1] 为右下角的矩形区域里的每个数都加上 a [ 1 ] [ 1 ] a[1][1] a[1][1],即
add(1, 1, 1, 1, a[1][1])
-
a [ 1 ] [ 2 ] a[1][2] a[1][2] 相当于以 a [ 1 ] [ 2 ] a[1][2] a[1][2] 为左上角、 a [ 1 ] [ 2 ] a[1][2] a[1][2] 为右下角的矩形区域里的每个数都加上 a [ 1 ] [ 2 ] a[1][2] a[1][2],即
add(1, 2, 1, 2, a[1][2])
-
…
-
a [ n ] [ m ] a[n][m] a[n][m] 相当于以 a [ n ] [ m ] a[n][m] a[n][m] 为左上角、 a [ n ] [ m ] a[n][m] a[n][m] 为右下角的矩形区域里的每个数都加上 a [ n ] [ m ] a[n][m] a[n][m],即
add(n, m, n, m, a[n][m])
这样,利用区间修改操作 a d d ( ) add() add() 即可完成赋初始值,从而避免了手动构造差分数组 b [ ] [ ] b[\ ][\ ] b[ ][ ]。