引入
如果给你n个数,然后进行q次询问,每次询问一个区间[x,y]的和,你会怎么做?
第一种方法:最简单的方法,用数组存起来,每次枚举x-y,ans加起来就可以,时间复杂度O(qn),十分慢。
第二种方法:或许大多数人会使用前缀和数组:sum[i]=a[1]+a[2]+…+a[i],所以求[x,y]只需要输出sum[y]-sum[x-1]即可,时间复杂度O(n),这是最快的方法之一了。
但是,如果加上一个条件:在q次询问中,有可能会临时使a[m]加上或减去一个数k(我们令这个为update(m,k)操作),也有可能会查询一个区间的和,怎么办呢?
如果还是用前缀和数组,就不方便了,因为update(m,k)需要更新sum[m]到sum[n]的值,于是时间复杂度又变为了O(qn)。
那么怎么办呢?于是有了树状数组。
树状数组
概念
树状数组,时间复杂度log级别的数据结构,且实现复杂度极小,不论是上面提到的update操作还是求前缀和。
如图,A数组是原始n个数的数组,C数组就是是树状数组(“树状”数组,是指一个普通数组,按树状存储,而不是一种STL中的数据结构)。
实现
观察一下有什么规律。
- C[1] = A[1]
- C[2] = C[1] + A[2] = A[1] + A[2]
- C[3] = A[3]
- C[4] = C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4]
- C[5] = A[5]
- C[6] = C[5] + A[6] = A[5] + A[6]
- C[7] = A[7]
- C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
不难发现,好像和二进制很有关系。
但是很难再想下去,事实上是这样的:
定义lowbit(x)为x二进制下末尾0的个数。
则含有C[
… …
(
如果没法理解,写一个循环就懂了:
for(int i=x;i<=n;i+=lowbit(i))
计算lowbit
lowbit(x)=x&-x
为什么?这里复制了一篇证明(懒得打)
首先明白一个概念,计算机中-i=(i的取反+1),也就是i的补码
而lowbit,就是求(树状数组中)一个数二进制的1的最低位,例如01100110,lowbit=00000010;再例如01100000,lowbit=00100000。
所以若一个数(先考虑四位)的二进制为abcd,那么其取反为(1-a)(1-b)(1-c)(1-d),那么其补码为(1-a)(1-b)(1-c)(2-d)。
如果d为1,什么事都没有-_-|||但我们知道如果d为0,天理不容2Σ( ° △ °|||)︴
于是就要进位。如果c也为0,那么1-b又要加1,然后又有可能是1-a……直到碰见一个为补码为0的bit,我们假设这个bit的位置为x
这个时候可以发现:是不是x之前的bit的补码都与其自身不同?,x之后的补码与其自身一样都是0?
例如01101000,反码为10010111,补码为10011000,可以看到在原来数正数第五位前,补码的进位因第五位使其不会受到影响,于是0&1=0,;
但在这个原来数“1”后,所有零的补码都会因加1而进位,导致在这个“1”后所有数都变成0,再加上0&0=0,所以他们运算结果也都是零;
只有在这个数处,0+1=1,连锁反应停止,所以这个数就被确定啦O(∩_∩)O
所以and以后只有x这个bit是一……
update操作
当要动态改变一个数时,用刚刚的循环枚举出与它相关的位置,都增加(减少)即可:
void update(int k,int x)
{
for(int i=k;i<=n;i+=lowbit(i))
C[i]+=x;
}
getsum操作
就是求前缀和,同样的,倒着进行刚刚的循环,累加路上的值即可:
int getsum(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i))//i要大于0
ans+=C[i];
return ans;
}
关于代码风格
树状数组的update和getsum基本是通用的,建议不要自己改函数名,lowbit可以写函数,也可以宏定义:#define lowbit(x) (x&-x)