To_Heart—总结——树状数组
树状数组主要用来解决区间求和问题。接下来,我们以区间求和为引子,详细讲述树状数组的两类操作。
问题引入
有一个一维数组,长度为n。
对这个数组做两种操作:
- 修改,对第i~j之间的某元素增加 v
- 求和,求 i 到 j 的和
首先,很容易想到前缀和,但是前缀和不支持修改操作,或者说,前缀和的修改操作的时间复杂度是O(n)。如果一共操作m次,则前缀和的最坏时间复杂度是O(nm),最好时间复杂度是O(m)。
但是,即将介绍的树状数组,是一种修改和求和都只需要O(logn)的算法,所以树状数组的最好和最坏时间复杂度都是O(m*logn)。
树状数组
接下来我们正式学习树状数组。
首先,我们先放一张图
这张图中的A数组为原数组,C数组为树状数组。
那么我们把每一个C[i]用A数组表示出来:
C[1]=A[1]
C[2]=A[1]+A[2]
C[3]=A[3]
C[4]=A[1]+A[2]+A[3]+A[4]
C[5]=A[5]
C[6]=A[5]+A[6]
C[7]=A[7]
C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]。
我们再将1~8这几个数进行2进制转换
1:0001
2:0010
3:0011
4:0100
5:0101
6:0110
7:0111
8:1000
i 转化成二进制数之后,只保留最低位的1及其后面的0,截断前面的内容,然后再转成十进制数,即为C[i]的子叶个数。如果我们设k为i的二进制中从最低位到高位连续零的长度,那么就可以写出以下公式
C i = A ( i − 2 k ) + 1 + A ( i − 2 k ) + 2 + . . . + A i C_i=A_{(i-2^k)+1}+A_{(i-2^k)+2}+...+A_{i} Ci=A(i−2k)+1+A(i−2k)+2+...+Ai
那么得到这个公式有什么用呢?
这样我们就可以在logn的时间复杂度求出1~i的前缀和。
举个例子。假设现在我们要找1~7元素的前缀和。
s u m 7 = A 1 + A 2 + A 3 + A 4 + A 5 + A 6 + A 7 sum_7=A_1+A_2+A_3+A_4+A_5+A_6+A_7 sum7=A1+A2+A3+A4+A5+A6+A7
s u m 7 = ( A 1 + A 2 + A 3 + A 4 ) + ( A 5 + A 6 ) + A 7 sum_7=(A_1+A_2+A_3+A_4)+(A_5+A_6)+A_7 sum7=(A1+A2+A3+A4)+(A5+A6)+A7
s u m 7 = C 4 + C 6 + C 7 sum_7=C_4+C_6+C_7 sum7=C4+C6+C7
而7的二进制是1110,一共有三个1,所以它的前缀和刚好可以用树状数组中的三个节点来表示。我们用k数组i在二进制中所有1的位置,即可得到以下公式:
s u m i = C i + C i − 2 k 1 + C ( i − 2 k 1 ) − 2 k 2 + . . . . . . sum_i=C_i+C_{i-2^{k_1}}+C_{(i-2^{k_1})-{2^{k_2}}}+...... sumi=Ci+Ci−2k1+C(i−2k1)−2k2+......
所以现在的问题就转换成如何求k了。
对于k的求法 有一个专门的名称,lowbit。它可以求出一个数二进制下的最后一个1.
lowbit写法
x&(-x)
首先,我们对x进行分类讨论,把它分为奇数和偶数的情况考虑。
在二进制下,数是以补码的形式储存的,而正数的补码就等于其原码,负数的补码等于原码取反加1。
如果原数是奇数,二进制下最后一位一定是1,取反后就变成0,加1以后又变成1,又由于其他位置都变成了相反数,所以两者相与的答案一定是1。
如果原数是偶数,二进制的最后一位一定是0,取反后是1,加1后进位变0,这个1一直进位直到前一位是0为止。当前一位是0的时候,则变为1。补码位置是0,说明原码位置是1,又因为0变成了1,1&1=1,所以两者相与的答案就是 2 k 2^k 2k。
综上,如果这个数是奇数,答案为1,即 2 0 2^0 20,偶数则为 2 k 2^k 2k,答案k都为此数的二进制中从最低位到高位连续零的长度。
代码如下
int Lowbit(ll x) { return x & (-x); }
如果还觉得不清晰,可以参考一下这篇博客
操作1:修改
因为前面的公式:
C i = A ( i − 2 k ) + 1 + A ( i − 2 k ) + 2 + . . . + A i C_i=A_{(i-2^k)+1}+A_{(i-2^k)+2}+...+A_{i} Ci=A(i−2k)+1+A(i−2k)+2+...+Ai
所以如果我们修改 A i A_i Ai,那么会影响 C i C_i Ci, C i + 2 k C_{i+2^k} Ci+2k, C ( i + 2 k 1 ) + 2 k 2 C_{(i+2^{k1})+2^{k2}} C(i+2k1)+2k2,……
所以只要沿着这个思路写就可以了
void HHup(ll k, ll x) {
for (int i = k; i <= n; i += Lowbit(i)) C[i] += x;
}
操作2:查询
直接查就可以了啊。。。。
不懂的自己往前在翻一翻。
ll HHsum(ll k) {
ll ans = 0;
for (int i = k; i >= 1; i -= Lowbit(i)) ans += C[i];
return ans;
}