- 本文参考OI Wiki 树状数组
原理
可以看到,$c_i$ 管理的数的数量就是 $i$ 的二进制表示中最低位的$1$所代表的数字
比如 $(2){10}=(10)_2$,最低位的 $1$ 代表 $2$ ,所以 $c_2$管理 $a_1,a_2$两个数字;$(6){10}=(110)_2$,最低位的 $1$ 表示的也是 $2$ ,所以 $c_6$ 管理 $a_5,a_6$ 两个数字
用法及操作
获取 $i$ 的二进制表示中最低位的 $1$ 所代表的值
#define lowbit(x) x & -x
比如 $(22){10}=(10110)_2$,$(-22){10}=(01010)$,所以$(10110)2\And(01010)_2=(00010)_2=(2){10}$
由于 $-x$ 为 对 $x$ 按位取反后加一得到,
按位取反后所有的 $1$ 变成 $0$ ,所有的 $0$ 变成 $1$ ,再加上 $1$ 之后会不断向前进位,直到遇到第一个 $0$ 可以“安置”这个不断进位来的 $1$ 。
而进位的时候低位的 $1$ 又变回了 $0$(与原码相同),进过位的数字只剩下 第一个遇上的 $0$ 变成的 $1$ 与原码同时为 $1$ (因此这部分除了第一个 $0$ 变成的 $1$ 与原码同时为 $1$,其他位均与原码同时为 $0$)
未进位的更高位的数字由于进行过按位取反且未被进位的 $1$ 影响到,所以这部分进行按位与时均为 $0$
好像说明白了又好像什么都没说再举个栗子,$(44)_{10}=(101100)_2$,对其进行按位取反得到 $(010011)_2$,这个二进制数可以分成两部分来看:
010
和011
,$(011)_2+(001)_2=(100)_2$,进位过的部分现在与原码的这一部分相同,这部分进行按位与的时候100&100=100
;未进位的前半部分010
由于与原码的101
是按位取反得到的,所以进行按位与的时候010&101=000
,所以这样可以得到最低位的 $1$所代表的值单点修改
将一个点的值加上 $k$,管理该点的所有的值都应该加上 $k$
1
2
3
4
5
6void add(int x,int k){
while(x <= n){
c[x] = c[x] + k;
x = x + lowbit(x);
}
}前缀求和
1
2
3
4
5
6
7
8int getSum(int x){
int ans = 0;
while(x >= 1){
ans = ans + c[x];
x = x - lowbit(x);
}
return ans;
}区间修改
维护一个差分数组
对于序列 $a_1,a_2,a_3,\cdots,a_n$,在区间 $[L,R]$ 内加上 $\Delta$,不难发现:
- $[L,R]$ 由于加上同一个数,相对大小是不变的,因此 $(L,R]$ 区间内的 $T$ 不需要修改。
- 而 $TL=a_L-a{L-1}$,$a{L-1}$ 大小不变,只有 $a_L$ 增加了 $\Delta$,因此 $T_L$ 增加了 $\Delta$。同理,$T{R+1}$ 减少 $\Delta$。
通过上述方法,就可以实现只修改 $TL$和 $T{R+1}$ 共两次就完成了 $[L,R]$ 的区间修改。时间复杂度大大降低。
我们只需用树状数组维护这个 $T$ 就可以了。
1
2
3
4
5
6
7
8void add(int x,int k){
while(x <= n){
T[x] = T[x] + k;
x = x + lowbit(x);
}
}
add(l,k);
add(r+1,-k);单点查询
输出差分数组前缀和即可
1
2
3
4
5
6
7
8int getSum(int x){
int ans = 0;
while(x >= 1){
ans = ans + T[x];
x = x - lowbit(x);
}
return ans;
}