一、树状数组是什么?
树状数组是一种支持 单点修改 和 区间查询 的,代码量小的数据结构。
二、工作原理
1.初步感受
其中a数组是原始数据数组,c数组可以看成管理a局部和的数组。例如
c
2
c_2
c2 管辖的是
a
[
1
…
2
]
a[1 \ldots 2]
a[1…2];
c
4
c_4
c4 管辖的是
a
[
1
…
4
]
a[1 \ldots 4]
a[1…4];
c
6
c_6
c6 管辖的是
a
[
5
…
6
]
a[5 \ldots 6]
a[5…6];
c
8
c_8
c8 管辖的是
a
[
1
…
8
]
a[1 \ldots 8]
a[1…8];
剩下的 c[x] 管辖的都是 a[x] 自己(可以看做
a
[
x
…
x
]
a[x \ldots x]
a[x…x] 的长度为 1 的小区间)。
2.求和举例
例如
a
[
1
…
7
]
a[1 \ldots 7]
a[1…7]的前缀和为
a
1
+
a
2
+
a
3
+
a
4
+
a
5
+
a
6
+
a
7
a_1 + a_2 + a_3 + a_4 + a_5 + a_6 + a_7
a1+a2+a3+a4+a5+a6+a7 =
c
4
+
c
6
+
c
7
c_4 +c_6 + c_7
c4+c6+c7
a
[
4
…
7
]
a[4 \ldots 7]
a[4…7]的前缀和为
a
[
1
…
7
]
−
a
[
1
…
3
]
a[1 \ldots 7] - a[1 \ldots 3]
a[1…7]−a[1…3] =
c
4
+
c
6
+
c
7
−
c
2
−
c
3
c_4 +c_6 + c_7 - c_2 - c_3
c4+c6+c7−c2−c3
3.管辖区间
树状数组中,规定 c [ x ] c[x] c[x]管辖的区间长度为 2 k 2^{k} 2k,其中:
设二进制最低位为第 0 位,则
k
k
k 恰好为
x
x
x 二进制表示中,最低位的 1 所在的二进制位数;
2
k
2^k
2k(
c
[
x
]
c[x]
c[x] 的管辖区间长度)恰好为
x
x
x 二进制表示中,最低位的 1 以及后面所有 0 组成的数
举个栗子
c
88
c_{88}
c88 管辖的是哪个区间?
因为
8
8
(
10
)
88_{(10)}
88(10)=
0101100
0
(
2
)
01011000_{(2)}
01011000(2),其二进制最低位的 1 以及后面的 0 组成的二进制是 1000,即 8,所以
c
88
c_{88}
c88 管辖 8 个
a
a
a 数组中的元素。因此,
c
88
c_{88}
c88 代表
a
[
81
…
88
]
a[81 \ldots 88]
a[81…88] 的区间信息。
我们记 x x x 二进制最低位 1 以及后面的 0 组成的数为 lowbit ( x ) \operatorname{lowbit}(x) lowbit(x),那么 c [ x ] c[x] c[x] 管辖的区间就是 [ x − lowbit ( x ) + 1 , x ] [x-\operatorname{lowbit}(x)+1, x] [x−lowbit(x)+1,x]。
注意这里的lowbit(x) = x & -x
int lowbit(int x) {
return x & -x;
}
4.区间查询
其实任何一个区间查询都可以这么做:查询 a [ l … r ] a[l \ldots r] a[l…r] 的和,就是 a [ 1 … r ] a[1 \ldots r] a[1…r] 的和减去 a [ 1 … l − 1 ] a[1 \ldots l - 1] a[1…l−1] 的和,从而把区间问题转化为前缀问题,更方便处理。
那么如何做前缀查询,回顾一下查询 a [ 1 … 7 ] a[1 \ldots 7] a[1…7] 的过程:
从 c 7 c_{7} c7 往前跳,发现 c 7 c_{7} c7 只管辖 a 7 a_{7} a7 这个元素;然后找 c 6 c_{6} c6,发现 c 6 c_{6} c6 管辖的是 a [ 5 … 6 ] a[5 \ldots 6] a[5…6],然后跳到 c 4 c_{4} c4,发现 c 4 c_{4} c4 管辖的是 a [ 1 … 4 ] a[1 \ldots 4] a[1…4]这些元素,然后再试图跳到 c 0 c_0 c0,但事实上 c 0 c_0 c0 不存在,不跳了。
我们刚刚找到的 c c c 是 c 7 , c 6 , c 4 c_7, c_6, c_4 c7,c6,c4,事实上这就是 a [ 1 … 7 ] a[1 \ldots 7] a[1…7]拆分出的三个小区间,合并一下,答案是 c 7 + c 6 + c 4 c_7 + c_6 + c_4 c7+c6+c4。
我们可以写出查询 a [ 1 … x ] a[1 \ldots x] a[1…x] 的过程:
从
c
[
x
]
c[x]
c[x] 开始往前跳,有
c
[
x
]
c[x]
c[x] 管辖
a
[
x
−
lowbit
(
x
)
+
1
…
x
]
a[x-\operatorname{lowbit}(x)+1 \ldots x]
a[x−lowbit(x)+1…x];
令
x
←
x
−
lowbit
(
x
)
x \gets x - \operatorname{lowbit}(x)
x←x−lowbit(x),如果
x
=
0
x = 0
x=0 说明已经跳到尽头了,终止循环;否则回到第一步。将跳到的
c
c
c 合并。
int getsum(int x) { // a[1]..a[x]的和
int ans = 0;
while (x > 0) {
ans = ans + c[x];
x = x - lowbit(x);
}
return ans;
}
5.单点修改
现在来考虑如何单点修改 a [ x ] a[x] a[x]。我们的目标是快速正确地维护 c c c 数组。为保证效率,我们只需遍历并修改管辖了 a [ x ] a[x] a[x] 的所有 c [ y ] c[y] c[y],因为其他的 c c c 显然没有发生变化。注意 c [ y ] c[y] c[y]必定包含 c [ x ] c[x] c[x]。
设 n n n 表示 a a a 的大小,不难写出单点修改 a [ x ] a[x] a[x] 的过程:
初始令
x
′
=
x
x' = x
x′=x。
修改
c
[
x
′
]
c[x']
c[x′]。
令
x
′
←
x
′
+
lowbit
(
x
′
)
x' \gets x' + \operatorname{lowbit}(x')
x′←x′+lowbit(x′),如果
x
′
>
n
x' > n
x′>n 说明已经跳到尽头了,终止循环;否则回到第二步。
void add(int x, int k) {
while (x <= n) { // 不能越界
c[x] = c[x] + k;
x = x + lowbit(x);
}
}
总结
树状数组有很多变种,这里不一一赘述,如有兴趣,可以参考https://oi.wiki/ds/fenwick/