先来看一道题。
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某一个数加上x
- 求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3个整数,表示一个操作,具体如下:
操作1: 格式:1 x k 含义:将第x个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例#1:
1 | 5 5 |
输出样例#1:
1 | 14 |
说明
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=8,M<=10
对于70%的数据:N<=10000,M<=10000
对于100%的数据:N<=500000,M<=500000
原题链接: https://www.luogu.org/problemnew/show/P3374
分析
这题我们如果用朴素的做法,则每次修改的复杂度为$O(1)$,每次查询的复杂度为$O(n)$,对于这题的数据规模难以承受。所以我们需要一种更为高效的数据结构,这里讲两个相对简单的,也就是树状数组和线段树。(写完树状数组感觉没时间再写了,所以线段树留到过两天再写)
对于这两种操作,树状数组和线段树的复杂度都是$O(logn)的。$
树状数组
基本思想
我们把整个区间分为若干个小区间,定义$C[i]$为区间第i个元素所维护的区间信息(比如在本题中,它所维护的区间信息就是这段小区间中所有数字的和)。
那么每个元素所维护的区间的具体范围是什么呢?先来看看这张图:
图片纯手绘。我们可以看到,$C[1]$维护的是[1, 1]的区间信息,而$C[2]$维护的是[1, 2]的区间信息,$C[3]$维护的是[3, 3]的区间信息,其他元素所维护的区间在图中也都能看出来。那么我们来找一下规律:
- 1转为二进制是0000 0001,它维护了大小为1的区间
- 2转为二进制是0000 0010,它维护了大小为2的区间
- 3转为二进制是0000 0011,它维护了大小为1的区间
- 4转为二进制是0000 0100,它维护了大小为4的区间
- 5转为二进制是0000 0101,它维护了大小为1的区间
- 6转为二进制是0000 0110,它维护了大小为2的区间
- 7转为二进制是0000 0111,它维护了大小为1的区间
- 8转为二进制是0000 1000,它维护了大小为8的区间
(这里的区间大小的意思是区间最右元素序号-区间最左元素序号+1)
有什么规律呢?规律就是:第$i$个元素序号转为二进制后,每位上要么是0要么是1,那么我们把位数最低的1所在的位数设为$x$,则这个元素就维护了大小为$2^x$的区间信息,记为$lowbit(i)$。可以证明,$lowbit(i) = i \& (-i)$。
结论就是: 第$i$个元素维护了大小为$lowbit(i)$的区间的信息。
单点修改,区间查询
回到刚才放的那张图,我们定义原序列的第$i$个元素为A[i],现在想要让A[1]+=1,那么C中的元素又该怎么变化呢?
由于$C[1]$维护的就是$A[1]~A[1]$的信息,因此$C[1]$一定也是要+=1的。$C[2]$维护的是$A[1]~A[2]$的信息,这段区间的信息也有了变化,所以$C[2]$也要+=1。可以发现,如果$C[i]$有变化,则$C[i+lowbit(i)]$也要有变化。那么我们就可以写出以下代码:1
2
3
4
5
6
7int c[500001], n; //n为整个序列包含的元素数量
int lowbit(int x) {
return x & (-x);
}
void add(int x, int v) { //将序列中第x个数+=v
for(; x <= n; x += lowbit(x)) c[x] += v;
}
修改完了元素,我们要查询某段区间中元素的和。如果我们想要查询区间[a, b]的和,我们只需要查询出[1, a]的和以及[1, b]的和,再用sum([1, b]) - sum([1, a]),就得到了[a, b]的和。
设[1, x]的和为$ans$,那么ans一定要+=c[x],也就是[x - lowbit(x) + 1, x]的区间和。从1到$x$有若干个小区间,我们加上[x - lowbit(x) + 1, x]的区间和后,让x-=lowbit(x),则问题就变为求[1, x - lowbit(x)]的区间和。对于这个问题我们仍可以用同样的方法解决它,最后就得到了[1, x]的区间和。代码如下:1
2
3
4
5int query(int x) { //查询[1, x]的区间和
int ans = 0;
for(; x >= 1; x -= lowbit(x)) ans += c[x];
return ans;
}
当我们要查询[a, b]的区间和时,我们只需要写query(b) - query(a - 1)
就可以了。
那么下面放上文章开头问题的AC代码: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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using namespace std;
int n, m, c[500100];
int lowbit(int x) {
return x & (-x);
}
void add(int x, int k) {
while (x <= n) {
c[x] += k;
x += lowbit(x);
}
}
int sum(int x) {
int ans = 0;
while (x >= 1) {
ans += c[x];
x -= lowbit(x);
}
return ans;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
add(i, x);
}
for (int i = 0; i < m; i++) {
int a, x, y;
scanf("%d%d%d", &a, &x, &y);
if (a == 1) add(x, y);
if (a == 2) printf("%d\n", sum(y) - sum(x - 1));
}
return 0;
}
区间修改,单点查询
对应的也有一道题。
题目描述
如题,已知一个数列,你需要进行下面两种操作:
- 将某区间每一个数数加上x
- 求出某一个数的值
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x 含义:输出第x个数的值
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例#1:
1 | 5 5 |
输出样例#1:
1 | 6 |
说明
时空限制:1000ms,128M
数据规模:
对于30%的数据:N<=8,M<=10
对于70%的数据:N<=10000,M<=10000
对于100%的数据:N<=500000,M<=500000
原题链接: https://www.luogu.org/problemnew/show/P3368
分析
这道题很多人用差分来做,其实还有另一种更简单的方法,就是把修改和查询操作反过来。
当我们修改某个区间的值时,我们采用刚才的区间查询的迭代方式,把遍历到的每个元素加上这个值,就像打上了标记一样。查询时,采用刚才的单点修改的迭代方式,返回的是遍历到的元素的和。具体代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15int n, c[500100];
int lowbit(int x) {
return x & (-x);
}
void add(int x, int k) {
for (; x >= 1; x -= lowbit(x)) c[x] += k;
}
int query(int x) {
int ans = 0;
for (; x <= n; x += lowbit(x)) ans += c[x];
return ans;
}
区间修改,区间查询
没想到还有这种黑科技,然而我不会,以后填坑。