To_Heart—总结——树状数组

树状数组主要用来解决区间求和问题。接下来,我们以区间求和为引子,详细讲述树状数组的两类操作。

问题引入

有一个一维数组,长度为n。

对这个数组做两种操作:

  1. 修改,对第i~j之间的某元素增加 v
  2. 求和,求 i 到 j 的和

首先,很容易想到前缀和,但是前缀和不支持修改操作,或者说,前缀和的修改操作的时间复杂度是O(n)。如果一共操作m次,则前缀和的最坏时间复杂度是O(nm),最好时间复杂度是O(m)。

但是,即将介绍的树状数组,是一种修改和求和都只需要O(logn)的算法,所以树状数组的最好和最坏时间复杂度都是O(m*logn)。

树状数组

接下来我们正式学习树状数组。

首先,我们先放一张图

1

这张图中的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(i2k)+1+A(i2k)+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+Ci2k1+C(i2k1)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(i2k)+1+A(i2k)+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;
}

猜你喜欢

转载自blog.csdn.net/xf2056188203/article/details/113857153