树状数组主要用于解决查询修改等区间操作的问题。
其实它也是线段树的一部分:线段树能做的,树状数组不一定能做;树状数组能做的,线段树一定能做(可能会比较慢)。
那么,树状数组的优点:
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 #include<iostream> 2 #include<vector> 3 #include<cstdio> 4 #include<queue> 5 #include<map> 6 #include<cstdlib> 7 #include<cmath> 8 #include<algorithm> 9 #include<set> 10 #include<cstring> 11 using namespace std; 12 typedef long long ll; 13 const ll INF=99999999; 14 const int MAXN=500002; 15 inline int lowbit(int x){ 16 return x&(-x); 17 } 18 int n,m; 19 int a[MAXN]; 20 int t[MAXN*4]; 21 void add(int p,int x){ 22 for(int i=p;i<=n;i+=lowbit(i)){ 23 //将之后的数组也更新,所以是+=lowbit 24 t[i]+=x; 25 } 26 } 27 int query(int p){ 28 int re=0; 29 for(int i=p;i>0;i-=lowbit(i)){ 30 //查询从起始到p的和 31 re+=t[i]; 32 } 33 return re; 34 } 35 int main() 36 { 37 ios_base::sync_with_stdio(false); 38 cin.tie(0); 39 40 cin>>n>>m; 41 for(int i=1;i<=n;i++){ 42 cin>>a[i]; 43 } 44 for(int i=1;i<=n;i++){ 45 //枚举每一个输入 46 for(int j=i-lowbit(i)+1;j<=i;j++){ 47 //从比i小lowbit(i)的地方(这里要+1,至于为什么 反正这样写是对的) 48 t[i]+=a[j]; 49 //构造数组 50 } 51 } 52 53 for(int i=1;i<=m;i++){ 54 int a,b,c; 55 cin>>a>>b>>c; 56 if(a==1){ 57 add(b,c); 58 } 59 if(a==2){ 60 cout<<query(c)-query(b-1)<<endl; 61 // 从后面-前面的,但是注意一定减后面的时候要-1 62 // 举个例子,从1到1,如果是query(1)-query(1)的话就是0了 63 } 64 } 65 return 0; 66 }
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)