树状数组
树状数组
- 本文参考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\)
void add(int x,int k){ while(x <= n){ c[x] = c[x] + k; x = x + lowbit(x); } }
-
前缀求和
int 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\) 不需要修改。
- 而 \(T_L=a_L-a_{L-1}\),\(a_{L-1}\) 大小不变,只有 \(a_L\) 增加了 \(\Delta\),因此 \(T_L\) 增加了 \(\Delta\)。同理,\(T_{R+1}\) 减少 \(\Delta\)。
通过上述方法,就可以实现只修改 \(T_L\)和 \(T_{R+1}\) 共两次就完成了 \([L,R]\) 的区间修改。时间复杂度大大降低。
我们只需用树状数组维护这个 \(T\) 就可以了。
void add(int x,int k){ while(x <= n){ T[x] = T[x] + k; x = x + lowbit(x); } } add(l,k); add(r+1,-k);
-
单点查询
输出差分数组前缀和即可
int getSum(int x){ int ans = 0; while(x >= 1){ ans = ans + T[x]; x = x - lowbit(x); } return ans; }