树状数组主要用于解决查询修改等区间操作的问题。
其实它也是线段树的一部分:线段树能做的,树状数组不一定能做;树状数组能做的,线段树一定能做(可能会比较慢)。
那么,树状数组的优点:
1.代码简洁好记。
2.由于使用位运算,跑得快。
当然也有缺点:
1.相对来说难以理解。
2.解决问题的广度不如线段树。
这个算法到底是怎样的?
树状数组的核心是lowbit。
lowbit是一个函数,lowbit(x)代表着x的二进制中从右往左数第一个1以及1之后的0组成的二进制数。
lowbit可以用简单的一行代码实现:
lowbit(x) = x*(-x)
至于具体原理,在此不表。
我们利用lowbit来将一个数组分成许多块,每一块维护着这个区间的信息。(分块?)
这里有一张图:
其中,红色线左边的棕色线代表这个元素维护的区间(块)。
比如,1的二进制是1,那么 lowbit(1) = 1,于是它维护 1 这个区间。
比如,4的二进制是100,那么 lowbit(4) = 4,于是它维护 1~4 这个区间。
比如,6的二进制是110,那么 lowbit(6) = 2,于是它维护 4~6这个区间。
. . . . . .
发现两个小规律:单数总是维护长度为1的块。原因也很简单,因为单数的二进制最右边一位永远是1,因此它们维护的长度也是1 。
而2的次方维护的总是以它自身为长度的区间,原因也很简单,因为2的次方在二进制中意味着最高一位是1,其余全是0 。
所以,形如1000000···这样的二进制,代表了从前往后到这个数所有元素,而1100000···代表了它的一半,1010000···则代表了它的1/4,以此类推,1000···1正好是个单数,维护了最小的区间,然后在它之上的1000···10则维护这个数本身和1000···1 。对半维护区间,是不是让你想起了线段树?而线段树是一个二叉树,意味着树状数组也是一个二叉树。只要画出它的右儿子,整棵树正是一棵二叉树:
蓝线是加上的右儿子。
因此,树状数组相对于线段树减少了一半的空间。
那么我们看代码:
代码:
1 #include2 #include 3 #include 4 #include 5 #include
1.单点加入
void add(int p,int x){
for(int i=p;i<=n;i+=lowbit(i)){
t[i]+=x;
}
}
注意从编号开始,是每次+=lowbit(i)操作。另外,t[i]是+=而不是=。
之所以每次加上lowbit能够“到达”下一个区间,原因在于树状数组本质上是一个二叉树。我们知道lowbit(i)代表这个元素维护的区间长度,加上自己为什么就能到达下一个区间呢?
原因是树状数组是一个左右相等的二叉树(不知道怎么说,原谅我的不严谨),所以左儿子加上它自己相当于到达了它的父亲。它的父亲维护两倍于它的区间。
2.区间查询
int query(int p){
int re=0;
for(int i=p;i>0;i-=lowbit(i)){
re+=t[i];
}
return re;
}
注意从p~1,是每次-=lowbit(i)。
从最右边往前跳。这里也可以使用 i != 0,原因是每次减去自身区间长度,最后一定会到达第0个元素。这是我们已经统计完毕,可以退出了。
3.初始化
for(int i=1;i<=n;i++){
for(int j=i-lowbit(i)+1;j<=i;j++){
t[i]+=a[j];
}
}
注意从i-lowbit(i)+1开始,j<=i;t[i]是+=。(i枚举每一个未处理的线性元素)
对于每一个区间加上区间加上其中的元素。
4.别忘了lowbit
inline int lowbit(int x){
return x&(-x);
}
这里使用内联函数(inline)会更好。
5.求区间值
求a到b的区间和的时候注意应该是query(b)-query(a-1)
query(b)-query(a-1)