【数据结构】树状数组

树状数组主要用于解决查询修改等区间操作的问题。

其实它也是线段树的一部分:线段树能做的,树状数组不一定能做;树状数组能做的,线段树一定能做(可能会比较慢)。

那么,树状数组的优点:

1.代码简洁好记。

2.由于使用位运算,跑得快。

当然也有缺点:

1.相对来说难以理解。

2.解决问题的广度不如线段树。

 

这个算法到底是怎样的?

树状数组的核心是lowbit。

lowbit是一个函数,lowbit(x)代表着x的二进制中从右往左数第一个1以及1之后的0组成的二进制数。

lowbit可以用简单的一行代码实现:

lowbit(x) = x*(-x)

至于具体原理,在此不表。

我们利用lowbit来将一个数组分成许多块,每一块维护着这个区间的信息。(分块?)

这里有一张图:

【数据结构】树状数组_第1张图片

其中,红色线左边的棕色线代表这个元素维护的区间(块)。

比如,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 。对半维护区间,是不是让你想起了线段树?而线段树是一个二叉树,意味着树状数组也是一个二叉树。只要画出它的右儿子,整棵树正是一棵二叉树:

 【数据结构】树状数组_第2张图片

蓝线是加上的右儿子。

因此,树状数组相对于线段树减少了一半的空间。

那么我们看代码:

 

代码:

 1 #include
 2 #include
 3 #include
 4 #include
 5 #include
 6 #include
 7 #include
 8 #include
 9 #include<set>
10 #include
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<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)

  

 

转载于:https://www.cnblogs.com/dudujerry/p/9858169.html

你可能感兴趣的:(【数据结构】树状数组)