最近看了很多篇关于如何刷题的文章,其中都不约而同的提到了"总结"这个关键词。仔细想想自己确实是从来没有做过一篇总结,刷过的题目就过去了,导致同样的题目再做一次又不会了。
我的第一篇总结就以最近刷到的「区间求和」问题展开吧。首先,先上一般问题的模板(其中加粗的为最佳方案):
- 数组不变,区间查询:前缀和、树状数组、线段树;
- 数组单点修改,区间查询:树状数组、线段树;
- 数组区间修改,单点查询:差分、线段树;
- 数组区间修改,区间查询:线段树。
前缀和
前缀和的作用就是为了帮助我们快速求某一段的和,是「差分」的逆运算。
前缀和数组的每一位记录的是当前位置距离起点位置,这连续一段的和区间和。
一维前缀和
假设有一个一维数组x和该数组的一维前缀和数组y,则x和y满足以下关系:
y0=x0、y1=x0+x1、y2=x0+x1+x2、......、yn=x0+x1+...+xn所以我们可以通过 yn=yn−1+xn 这个公式计算出前缀和,代码实现如下:
1
2
3
4
| for (int i = 0; i < n; i++) {
if (i == 0) y[i] = x[i];
else y[i] = y[i - 1] + x[i];
}
|
但是在实际使用中,常常会遇到左边界溢出的情况,为了避免这种情况,我们可以将前缀和数组整体向后移动一位,下面给出前缀给计算代码:
1
2
3
4
| int n = x.size();
vector<int> y(n + 1);
for (int i = 1; i <= n; i++)
y[i] = y[i - 1] + x[i - 1];
|
这样当我们想求区间[a,b]之和时只需要计算y[b+1]−y[a]即可。
二维前缀和
有一个二维数组a和该数组的二维前缀和数组b(其同样是个二维数组)
右侧标注橙色的二维前缀和元素,其值是左侧的原二维数组中标注橙色的所有元素的和。
二维前缀和二维前缀和实际上就是一个矩阵内值的和,而矩阵又可以由两个行数或列数少一的子矩阵组合后,删去重合部分再加上右下角的值来构成,也就是以下式子:
bx,y=bx−1,y+bx,y−1−bx−1,y−1+ax,y详细的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
| for (int y = 0; y < n; y++) // n行
for (int x = 0; x < m; x++) // m列
{
if (x == 0 && y == 0)
b[y][x] = a[y][x]; //左上角的值
else if (x == 0)
b[y][x] = b[y - 1][x] + a[y][x]; //第一列
else if (y == 0)
b[y][x] = b[y][x - 1] + a[y][x]; //第一行
else
b[y][x] = b[y - 1][x] + b[y][x - 1] - b[y - 1][x - 1] + a[y][x];
}
|
通过二维前缀和,我们可以很方便的计算出给定矩阵的和,即上面求前缀和的逆运算。假设所求矩阵的左上角点为(x1,y1),右下角点为(x2,y2),则有公式:
sum=B[y2][x2]+B[y1−1][x1−1]−B[y1−1][x2]−B[y2][x1−1]
这里结合图形会更好理解一点。
例题
力扣303. 区域和检索 - 数组不可变
就是一维前缀和的模板题,直接上模板即可做出。
差分
差分是一种处理数据的巧妙而简单的方法,它应用于区间的修改和询问问题。如果我们要经常对数组A某个区间内的所有元素做相同的加减操作,如果一个一个修改效率很差,但是使用差分数组后,这个操作的时间复杂度就能降到O(1),当所有的修改操作结束后,再利用差分数组,计算出新的A。
一维差分
我们定义原数组为a[]、差分数组为D[],则有计算公式:D[k]=a[k]−a[k−1],即原数组a[]的相邻元素的差。从定义我们可以推出a[k]=D[0]+D[1]+…+D[k]。也就是说a[]是D[]的前缀和,所以「差分」和「前缀和」就是一组逆运算。
当我们想要对区间[L,R]每个元素加上d,则只要进行下面的两步操作:
1
2
| D[L] += d;
D[R+1] -= d;
|
当所有修改都完成时,我们可以通过复杂度为O(n)的操作计算出新的a[]
当然,差分也不是万能的,它只能解决 区间修改+单点查询 这类问题。当遇到需要多次查询的问题,比如有m次修改,k次查询,此时修改的复杂度为O(m),查询的复杂度为O(kn),总复杂度为O(m+kn),和暴力法O(mn+k)的复杂度一致了。
二维差分
从一维差分可以很容易的推广到二维差分,我们可以通过下面的公式计算D[][]:
D[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]我们想修改坐标点(x1,y1)~(x2,y2)定义的区间,则需要修改矩阵的四个点:
1
2
3
4
| D[y1][x1] += d; //二维区间的起点
D[y1][x2+1] -= d; //把y看成常数,x从x1到x2+1
D[y2+1][x1] -= d; //把x看成常数,y从y1到y2+1
D[y2+1][x2+1] += d; //由于前两式把d减了2次,多减了1次,这里加1次回来
|
前缀和a[][]的计算可以用下面的公式:
a[i][j]=D[i][j]+a[i−1][j]+a[i][j−1]−a[i−1][j−1]在计算a[][]时,我们可以不新开一个数组,而是以用过的D[][]作为a[][],以此来节约空间。使用的话只需要将上面式子中的a全部换为D
树状数组
树状数组的优点在于查询和修改的时间复杂度都为O(logn),且比线段树好写。
单点修改、区间查询
树状数组的原理由于时间有限,这里就不展开说了(之后有空再补上😇),先上模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| int c[MAXN]; //树状数组(下标从1开始)
int n; //数组的长度
inline int lowbit(int i) {
return i & (-i);
}
//在位置i加上k
void update(int i, int k) {
while (i <= n) {
c[i] += k;
i += lowbit(i);
}
}
//求A[1]~A[i]的和
int getsum(int i) {
int res = 0;
while (i > 0) {
res += c[i];
i -= lowbit(i);
}
return res;
}
|
这里一定要注意,树状数组和原数组下标都是从1开始的。
由此就解决了单点修改、区间查询的问题,我们想要得到区间(a,b)的和,只需使用getsum(b) - getsum(a - 1)
。
例题:P3374【模板】树状数组 1
区间修改、单点查询
在上面的差分中我们分析了,如果有多次查询,仅仅使用差分的时间复杂度就和暴力差不多了。这时,我们可以采用树状数组+差分的方法。
我们将上面模板中的原始数组改为差分数组。由差分的性质可知,当我们修改一个区间时,只需要修改区间的两个端点即可,此时区间修改的问题便被转化成了单点修改的问题。同时,树状数组前n项和也就变成了原数组第n项的值。
例题:P3368【模板】树状数组 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| //前面还是树状数组的模板,这里就不放了
int main() {
cin >> n >> m;
int pre = 0, now;
for (int i = 1; i <= n; i++) {
cin >> now;
//这里每次求差分来生成树状数组
update(i, now - pre);
pre = now;
}
while (m--) {
int a, x, y, k;
cin >> a >> x;
if (a == 1) {
cin >> y >> k;
//更新即为对D[X]、D[y+1]进行修改
update(x, k);
update(y + 1, -k);
} else if (a == 2) {
//求D[1]~D[i]的和,即A[i]的值
cout << getsum(x) << endl;
}
}
system("pause");
return 0;
}
|
参考资料