树状数组是一个简单而且好用的数据结构,只有更新和查询操作,都可以在log(n)的复杂度内完成操作。
其C++实现代码为:
int lowbit(int x){
return x&(-x);
}
void add(int i,int x){
for(;i <= n;i+=lowbit(i)){
a[i]+=x;
}
}
int sum(int i){
int ans = 0;
for(;i>0;i-=lowbit(i)){
ans += a[i];
}
return ans;
}
lowbit()函数
lowbit()函数有什么作用呢?返回那个数二进制最后一位1所代表的数字,也就是2^k。比如,lowbit(2)是2,2的二进制是10,lowbit(15)是1,15的二进制是1111。lowbit()函数使得数组的下标具有了父子关系,可以通过加减lowbit()的返回值来访问当前下标的父亲或儿子,使连续的数组具有类似树的结构。
要想明白lowbit()函数的实现与计算机以补码储存数有关.正数的补码就是他本身,负数的补码要将其二进制按位取反然后加1。举个例子,+5的补码为0101,-5的补码为1011;+6的补码为0110,-6的补码为1010。很容易发现,如果将正数与其对应的负数进行与操作,返回的正好是其二进制最后一位1所代表的值,5&(-5)=1,6&(-6)=2,lowbit()函数就是用这种方法实现的。
树状数组实现
数组是具有连续下标的序列,树是由点组成的具有层次关系的集合,树状数组是这两种关系的集合,对于两个下标x,y,如果x+2^k=y,则称y是x的父节点,x是y的子节点,lowbit()函数就是用来快速求2^k的。
树状数组维护的是区间和,区间的右端点显而易见就是当前坐标i,左端点则是通过树找到的最左儿子,那么求和时只需将小于i的所有区间加到一起就是1~i的区间和。
下面我们将利用树状数组解决一个简单的问题来理解树状数组:
现在有9堆石子,编号分别为1~9,每堆有对应其编号的石子数,现在问第5~9有多少堆石子。
这个问题很简单,如果我们用树状数组来解决这个问题,则刚好涉及到了树状数组的两个操作,add()函数和sum()函数。问题的答案为sum(9)-sum(4)。
我们维护一个数组a
add()操作
将第一堆插入到数组中
a[1]+=1;lowbit(1)=1,a[2]+=1;lowbit(2)=2,a[4]+=1;lowbit(4)=4,a[8]+=1;
将第二堆插入到数组中
a[2]+=2;a[4]+=2;a[8]+=2;
将第三堆插入到数组中
a[3]+=3;lowbit(3)=1,a[4]+=3;a[8]+=3;
将第四堆插入到数组中
a[4]+=4;a[8]+=4;
将第五堆插入到数组中
a[5]+=5;lowbit(5)=1,a[6]+=5;lowbit(6)=2,a[8]+=5;
将第六堆插入到数组中
a[6]+=6;a[8]+=6;
将第七堆插入到数组中
a[7]+=7;lowbit(7)=1,a[8]+=7;
第八堆,第九堆插入到数组中
a[8]+=8;a[9]+=9;
sum()操作
lowbit(9)=1,9-lowbit(9)=8,8-lowbit(8)=0;
sum[9] = a[9]+a[8] = 9+36 = 45;
lowbit(4)=4,4-lowbit(4) = 0;
sum(4) = a[4] = 10;
答案为35。
总结
我所理解的树状数组大致是这样了,树状数组还有很多巧妙的用法,要学习的还有很多。在应用中要注意树状数组是从下标1开始的,要避免0的情况出现,会死循环,我的解决方法是预处理下数据,比如全都加个1,可能还有更好的办法吧。如果觉得我讲的不够详细可以看看这篇博客,博客后面还有练习题。