刘汝佳の树状数组详解

引入

二叉索引树,也叫树状数组是一种便于数组单点修改和区间求和的数据结构
主要根据下标的lowbit值来建树
至于lowbit(x),则是(x)&(-(x)),也就是一个二进制数从右边数第一个1代表的数

#define lowbit(x) ((x)&(-(x)))

基础树状数组如下图所示
刘汝佳の树状数组详解_第1张图片
灰色结点为树状数组中的结点,不难发现,lowbit值相同的结点在同一层上,lowbit值越大越靠近根
我们用一个c数组来存储图中白色长条(最下面结点所代表的区间和就是其本身)的区间和
不难看出
c i = ∑ j = i − l o w b i t ( i ) i a j c_i=\sum_{j=i-lowbit(i)}^ia_j ci=j=ilowbit(i)iaj
在有了以上理论基础后,我们来研究一下树状数组中的相关操作

树状数组的相关操作

单点修改

当我们要修改树状数组中的其中一个结点时,我们不仅需要改这个单点的值,还要修改c数组的值(也就是白色长条所代表的值),我们发现,这个结点只有其正上方的白色长条覆盖着,所以只需要一边往上爬一边修改沿路的c就可以了,如图所示
刘汝佳の树状数组详解_第2张图片

代码

void add(int x,int k){
	while(x<=n){
		c[x]+=k;//对沿路的c数组进行修改
		x+=lowbit(x);//往上爬,若是这一操作无法理解的话,推荐读者打一下草
	}
}

求前缀和

为了求一段前缀和,我们可以在图中发现我们需要往左上一直爬并加上沿路的c数组
如图所示
刘汝佳の树状数组详解_第3张图片

代码

int query(int x){
	int sum=0;
	while(x>0){
		sum+=c[x];//加上沿路的c数组
		x-=lowbit(x);//往左上爬
	}
	return sum;
}

区间修改与区间和

接下来讲解的就是树状数组中比较难理解的区间操作
区间操作需要的是差分
注:接下来的操作推荐读者与笔者一同推算
d i f 1 = a 1 , d i f 2 = a 2 − a 1 , d i f 3 = a 3 − a 2 . . . d i f i = a i − a i − 1 dif_1=a_1,dif_2=a_2-a_1,dif_3=a_3-a_2...dif_i=a_i-a_{i-1} dif1=a1,dif2=a2a1,dif3=a3a2...difi=aiai1
特别地, a 0 = 0 a_0=0 a0=0
如果用dif表示出a来,那么可以发现 a i = ∑ j = 1 i d i f j a_i=\sum_{j=1}^idif_j ai=j=1idifj
所以得出前缀和
s u m i = ∑ j = 1 i ∑ k = 1 k d i f k sum_i=\sum_{j=1}^i\sum_{k=1}^kdif_k sumi=j=1ik=1kdifk
= ( d i f 1 ) + ( d i f 1 + d i f 2 ) + . . . + ( d i f 1 + d i f 2 + . . . + d i f i ) =(dif_1)+(dif_1+dif_2)+...+(dif_1+dif_2+...+dif_i) =(dif1)+(dif1+dif2)+...+(dif1+dif2+...+difi)
= i ∗ d i f 1 + ( i − 1 ) ∗ d i f 2 + . . . + 1 ∗ d i f i =i*dif_1+(i-1)*dif_2+...+1*dif_i =idif1+(i1)dif2+...+1difi
= i ∗ ( d i f 1 + d i f 2 + . . . + d i f i ) − ( 0 ∗ d i f 1 + 1 ∗ d i f 2 + . . . ( i − 1 ) ∗ d i f i ) =i*(dif_1+dif_2+...+dif_i)-(0*dif_1+1*dif_2+...(i-1)*dif_i) =i(dif1+dif2+...+difi)(0dif1+1dif2+...(i1)difi)
= i ∗ ∑ j = 1 i d i f j − i ∗ ∑ j = 1 i ( j − 1 ) ∗ d i f j =i*\sum_{j=1}^idif_j-i*\sum_{j=1}^i(j-1)*dif_j =ij=1idifjij=1i(j1)difj
然后分别用树状数组T1,T2存这两项
那么在区间修改时,如何对T1,T2修改呢?举个例子
对序列 1 , 2 , 3 , 4 , 5 1,2,3,4,5 1,2,3,4,5的[2,4]进行+1操作
操作后的序列为 1 , 3 , 4 , 5 , 5 1,3,4,5,5 1,3,4,5,5
原序列的查分序列为 1 , 1 , 1 , 1 , 1 1,1,1,1,1 1,1,1,1,1
而操作后的查分序列为 1 , 2 , 1 , 1 , 0 1,2,1,1,0 1,2,1,1,0
可以看到,只需要更改第 l l l项和第 r + 1 r+1 r+1项即可,再对应到T1,T2的具体含义中,就可以进行修改操作了
区间[l,r]和
s u m r − s u m l − 1 sum_r-sum_{l-1} sumrsuml1
= [ r ∗ ∑ j = 1 r d i f j − r ∗ ∑ j = 1 r ( j − 1 ) ∗ d i f j ] − [ ( l − 1 ) ∗ ∑ j = 1 l − 1 d i f j − ( l − 1 ) ∗ ∑ j = 1 l − 1 ( j − 1 ) ∗ d i f j ] =[r*\sum_{j=1}^rdif_j-r*\sum_{j=1}^r(j-1)*dif_j]-[(l-1)*\sum_{j=1}^{l-1}dif_j-(l-1)*\sum_{j=1}^{l-1}(j-1)*dif_j] =[rj=1rdifjrj=1r(j1)difj][(l1)j=1l1difj(l1)j=1l1(j1)difj]

代码

struct BIT{
	static const int M=1e5+5;
	int c[M];
	void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
	int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
//区间修改
void build(int x,int dif){
	T1.add(x,dif);
	T2.add(x,(x-1)*dif);
}
void interval_add(int l,int r,int k){
	T1.add(l,k),T1.add(r+1,-k);
	T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
//区间查询
void interval_query(int l,int r){
	return (r*T1.query(r)-(l-1)*T1.query(l-1))-(T2.query(r)-T2.query(l-1));
}

例题

P3374 【模板】树状数组 1

刘汝佳の树状数组详解_第4张图片

思路

这题的数据还是比较水的,所以笔者直接暴力用前缀和过了

代码

#include
using namespace std;
const int M=1e6+5;
#define int long long
#define lowbit(x) (x)&(-(x))
int n,m;
int c[M];
void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
signed main()
{
	cin>>n>>m;
	for(int i=1,p;i<=n;i++) cin>>p,add(i,p);
	while(m--){
		int if_case;cin>>if_case;
		int x,y,k;
		switch (if_case){
			case 1:cin>>x>>k;add(x,k);break;
			case 2:cin>>x>>y;cout<<query(y)-query(x-1)<<endl;break;//暴力前缀和做法
		}
	}
	return 0;
}

P3368 【模板】树状数组 2

刘汝佳の树状数组详解_第5张图片

思路

这个题就需要区间操作了,需要的是区间修改
我们因为T1和T2存的都是查分,所以需要加一点操作

a i = ∑ j = 1 i d i f j a_i=\sum_{j=1}^idif_j ai=j=1idifj

所以查询第x个数时直接输出T1的前缀和就行
理论存在,实践开始

代码

#include
using namespace std;
#define int long long
#define lowbit(x) ((x)&(-(x)))
int n,m;
struct BIT{
	static const int M=5e5+5;
	int c[M];
	void add(int x,int k) { while(x<=n) c[x]+=k,x+=lowbit(x); }
	int query(int x) { int sum=0;while(x>0) sum+=c[x],x-=lowbit(x);return sum; }
}T1,T2;
void build(int x,int dif) { T1.add(x,dif),T2.add(x,(x-1)*dif); }
void interval_add(int l,int r,int k){
	T1.add(l,k),T1.add(r+1,-k);
	T2.add(l,k*(l-1)),T2.add(r+1,-k*(r+1-1));
}
int query(int x) { return T1.query(x); }
signed main()
{
	cin>>n>>m;
	int a,c=0;
	for(int i=1;i<=n;i++){
		cin>>a;
		c=a-c;
		build(i,c);
		c=a;
	}
	while(m--){
		int if_case;
		cin>>if_case;
		int x,y,k;
		switch (if_case){
			case 1:cin>>x>>y>>k;interval_add(x,y,k);break;
			case 2:cin>>x;cout<<query(x)<<endl;break;
		}
	}
}

end

参考资料:刘汝佳·《算法竞赛入门经典训练指南》
就这样,树状数组讲解完了,笔者不多赘述
完结撒花

你可能感兴趣的:(算法,数据结构,c++,树状数组,刘汝佳)