preparing

树状数组

树状数组

简介

树状数组和线段树具有相似的功能,但他俩毕竟还有一些区别:树状数组能有的操作,线段树一定有;线段树有的操作,树状数组不一定有。但是树状数组的代码要比线段树短,思维更清晰,速度也更快,在解决一些单点修改的问题时,树状数组是不二之选。--选自\(OI-WIKI\)


如图,\(a\)数组为原序列,\(c\)数组为树状数组,可以得到以下规律:

\(c_1=a_1\)
\(c_2=a_1+a_2\)
\(c_3=a_3\)
\(c_4=a_1+a_2+a_3+a_4\)
\(c_5=a_5\)
\(c_6=a_5+a_6\)
\(c_7=a_7\)
\(c_8=a_1+a_2+a_3+a_4+a_5+a_6+a_7+a_8\)

看上去没有什么规律,但是我们将下标转化为二进制以后:

\(c_{(0001)_2}=a_{(0001)_2}\)
\(c_{(0010)_2}=a_{(0001)_2}+a_{(0010)_2}\)
\(c_{(0011)_2}=a_{(0011)_2}\)
\(c_{(0100)_2}=a_{(0001)_2}+a_{(0010)_2}+a_{(0011)_2}+a_{(0100)_2}\)
\(c_{(0101)_2}=a_{(0101)_2}\)
\(c_{(0110)_2}=a_{(0101)_2}+a_{(0110)_2}\)
\(c_{(0111)_2}=a_{(0111)_2}\)
\(c_{(1000)_2}=a_{(0001)_2}+a_{(0010)_2}+a_{(0011)_2}+a_{(0100)_2}+a_{(0101)_2}+a_{(0110)_2}+a_{(0111)_2}+a_{(1000)_2}\)

这时可能就能发现有规律了,我们先引入定义:
定义:一个数\(i\)的二进制中最低位的\(1\)的位置称为\(i\)\(lowbit\),如\(lowbit((10010110)_2)=(00000010)_2=2\),同理\(lowbit(5)=1,lowbit(6)=2,lowbit(8)=8\),也就是说,\(lowbit(i)=\min\limits_{k\in \N,2^k\mid x}{2^k}\)
那么,我们可以发现,\(c_i=\sum\limits_{x=i-lowbit(i)+1}^{i}a_x\)
\(lowbit\)的求法有很多,最常用的是:\(lowbit(i)=i\&(-i)\)

因为负数用补码存储,即将这个数的二进制取反并加\(1\),如\(10\)的二进制为\(1010\),取反得到反码\(0101\),加\(1\)得到补码\(0110\),此时\((1010)\&(0110)=0010=(2)_{10}=lowbit(10)\)。那么,对于所有的\(i\),其反码与原码相反,\(+1\)后会使最低位的原码\(1\)处的值变成\(1\),故按位与后得到\(lowbit(i)\)

  • 代码
int lowbit(int x){
	return x&(-x);
}

单点修改 + 区间查询

单点修改


如图,假设我们要修改\(a_3\)的值,那么相应改变的有\(c_3,c_4,c_8\),我们发现\(3+lowbit(3)=4,4+lowbit(4)=8\),所以我们得到,改变\(a_i\)的值就要改变\(c_{x_1},c_{x_2},…,c_{x_n}(x_1=i,x_j=x_{j-1}+lowbit(x_{j-1}),x_j\in[1,n])\)

  • 代码
void pluss(int dir,int num){  //a[dir]加上num
	for(int i=dir;i<=n;i+=lowbit(i)){
		c[i]+=num;
	}
}

区间查询

相应的,我们可能要查询\([1,i]\)区间的值的和(后文无特殊说明外,本部分中\([l,r]\)表示\(\sum\limits_{i=l}^{r}{a_i}\)),类比前缀和,\([l,r]\)等于\([1,r]-[1,l-1]\)

假如我们要求\([1,7]\),我们相当于是求\([1,4]+[5,6]+[7,7]\),即\(c_4+c_6+c_7\),注意到\(6=7-lowbit(7),4=6-lowbit(6)\),类比修改我们得到求区间\([1,i]\)内的值的和\([1,i]=c_{x_1}+c_{x_2}+…+c_{x_n}(x_1=i,x_j=x_{j-1}-lowbit(x_{j-1}),x_j\in[1,n])\)

  • 代码
int search(int dir){
	int ans=0;
	for(int i=dir;i>=1;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}

模板#1:P3374

  • 代码
/*
c[1]=a[1]
c[2]=a[1]+a[2]
c[3]=a[3]
c[4]=a[1]+a[2]+a[3]+a[4]
c[5]=a[5]
c[6]=a[5]+a[6]
c[7]=a[7]
c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
...
c[n]=a[n-k+1]+a[n-k+2]+...+a[n] (k=lowbit(n))

sumn=c[n]+c[n-k1]+c[(n-k1)-k2]+...(ki=lowbit(n-k1-k2-...-k(i-1))) 

a[n]包含于c[n+k1],c[n+k1+k2],...(ki=lowbit(n+k1+k2+...+k(i-1))) 
*/
#include<iostream>
#include<cstdio>
#define maxn 500005
using namespace std;
int n,m,opt,x,y,c[maxn];
int lowbit(int x){
	return x&(-x);
}
void pluss(int dir,int num){
	for(int i=dir;i<=n;i+=lowbit(i)){
		c[i]+=num;
	}
}
int search(int dir){
	int ans=0;
	for(int i=dir;i>=1;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&x);
		pluss(i,x);
	}
	while(m--){
		scanf("%d%d%d",&opt,&x,&y);
		if(opt==1){
			pluss(x,y);
		}else{
			printf("%d\n",search(y)-search(x-1));
		}
	}
	return 0;
}

区间修改 + 单点查询

区间修改

此时我们引入差分,将区间修改转化为单点修改。
定义差分数组\(d\)\(d_i=a_i-a_{i-1}(i\in[1,n])\)
比如说\(a\)数组为{\(1,5,4,2,3\)},则差分数组\(d\)为{\(1,4,-1,-2,1\)}
此时若将\([2,4]\)区间内的值加\(2\),则改变后的\(a\)为{\(1,7,6,4,3\)},\(d\)为{\(1,6,-1,-2,-1\)}
此时可以发现,将\(a\)\([2,4]\)的值加了\(2\)\(d\)的变化为:\(d_2+2,d_5-2\),其余不变。类比一下,我们可以得出,\([l,r]\)的值加\(k\)\(d\)数组的变化为:\(d_l+k,d_{r+1}-k\),其余不变

证明也很简单,将\([l,r]\)的值加\(k\),则\(d_l\)的值从\(a_l-a_{l-1}\)变成了\((a_l+k)-a_{l-1}\),增加了\(k\)\(d_{r+1}\)的值从\(a_{r+1}-a_r\)变成了\(a_{r+1}-(a_r+k)\),减小了\(k\);而\(\forall x\in[l+1,r]\),值从\(a_x-a_{x-1}\)变成了\((a_x+k)-(a_{x-1}+k)\),不变。

  • 代码
//此时用树状数组维护差分数组d
void pluss(int dir,int num){
	for(int i=dir;i<=n;i+=lowbit(i)){
		d[i]+=num;
	}
}
…
scanf("%d",&opt);
if(opt==1){
	scanf("%d%d%d",&x,&y,&z);
	pluss(x,z);
	pluss(y+1,-z);
}

单点查询

由于有了差分数组,那么单点查询就非常简单了,因为\(d_i=a_i-a_{i-1}\)所以\(a_i=(a_i-a_{i-1})+(a_{i-1}-a_{i-2})+…+(a_2-a_1)+(a_1-a_0)=d_i+d_{i-1}+…+d_1\),即\(a_i=\sum\limits_{j=1}^{i}{d_j}\)
于是我们就可以用刚刚的区间查询查\([1,i]\)的值的和来求出\(a_i\)了(树状数组维护的是差分数组\(d\)

  • 代码
int search(int dir){
	int ans=0;
	for(int i=dir;i>=1;i-=lowbit(i)){
		ans+=d[i];
	}
	return ans;
}
…
scanf("%d",&opt);
if(opt==1){
        …
}else{
	scanf("%d",&x);
	printf("%d\n",search(x));
}

模板#2:P3368

  • 代码
/*
差分 d[i]=a[i]-a[i-1]
a[i]=d[1]+d[2]+...+d[i]
*/
#include<iostream>
#include<cstdio>
#define maxn 500005
using namespace std;
int n,m,opt,x,y,z,d[maxn],pre;
int lowbit(int x){
	return x&(-x);
}
void pluss(int dir,int num){
	for(int i=dir;i<=n;i+=lowbit(i)){
		d[i]+=num;
	}
}
int search(int dir){
	int ans=0;
	for(int i=dir;i>=1;i-=lowbit(i)){
		ans+=d[i];
	}
	return ans;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&x);
		pluss(i,x-pre);
		pre=x;
	}
	while(m--){
		scanf("%d",&opt);
		if(opt==1){
			scanf("%d%d%d",&x,&y,&z);
			pluss(x,z);
			pluss(y+1,-z);
		}else{
			scanf("%d",&x);
			printf("%d\n",search(x));
		}
	}
	return 0;
}

区间修改 + 区间查询

这是线段树最烦的部分,但树状数组还是轻松解决
此时,我们顺着刚刚的思路,因为要求\([1,n]\)\(\sum\limits_{i=1}^{n}{a_i}\)的值(同上\([l,r]\)可以用\([1,r]-[1,l-1]\)求),我们先将其变形:

\[\sum\limits_{i=1}^{n}{a_i}=\sum\limits_{i=l}^{n}\sum\limits_{j=1}^{i}{d_j} \]

我们可以发现,在这个式子中\(d_1\)被加了\(n\)次,\(d_2\)被加了\((n-1)\)次,…,\(d_x\)被加了\((n-x+1)\)次,故:

\[\sum\limits_{i=1}^{n}{a_i}=\sum\limits_{i=l}^{n}\sum\limits_{j=1}^{i}{d_j}=\sum\limits_{i-1}^{n}{[(d_i)*(n-i+1)]}=(n+1)\times\sum\limits_{i-1}^{n}{d_i}-\sum\limits_{i-1}^{n}{(d_i\times i)} \]

所以,我们可以用树状数组维护\(\sum\limits_{i-1}^{n}{d_i}\)\(\sum\limits_{i-1}^{n}{(d_i\times i)}\),不妨令其分别为\(sum1\)\(sum2\),就得到了区间修改与区间查询的方法:

区间修改

同上,\(sum1\)的修改同“区间修改 + 单点查询”中数组\(d\)的修改;
\(sum2\)的修改即乘上对应的\(i\)即可

  • 代码
void pluss(ll dir,ll num){
	for(int i=dir;i<=n;i+=lowbit(i)){
		sum1[i]+=num;
		sum2[i]+=num*dir;
	}
}
…
scanf("%lld",&opt);
if(opt==1){
	scanf("%lld%lld%lld",&x,&y,&z);
	pluss(x,z);
	pluss(y+1,-z);
}

区间查询

\([1,n]\)的值的和即为\((n+1)\times\sum\limits_{i-1}^{n}{sum1_i}-\sum\limits_{i-1}^{n}{sum2_i}\)
\([l,r]\)的值的和即为\([1,r]-[1,l-1]\)

  • 代码
ll search(ll dir){
	ll ans=0;
	for(ll i=dir;i>=1;i-=lowbit(i)){
		ans+=(dir+1)*sum1[i]-sum2[i];
	}
	return ans;
}
…
scanf("%lld",&opt);
if(opt==1){
	…
}else{
	scanf("%lld%lld",&x,&y);
	printf("%lld\n",search(y)-search(x-1));
}

模板#3:P3372

  • 代码
/*
sum1 维护 d[i]
sum2 维护 d[i]*i
*/
#include<iostream>
#include<cstdio>
#define maxn 500005
#define ll long long
using namespace std;
ll n,m,opt,x,y,z,sum1[maxn],sum2[maxn],pre;
ll lowbit(ll x){
	return x&(-x);
}
void pluss(ll dir,ll num){
	for(int i=dir;i<=n;i+=lowbit(i)){
		sum1[i]+=num;
		sum2[i]+=num*dir;
	}
}
ll search(ll dir){
	ll ans=0;
	for(ll i=dir;i>=1;i-=lowbit(i)){
		ans+=(dir+1)*sum1[i]-sum2[i];
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&m);
	for(ll i=1;i<=n;i++){
		scanf("%lld",&x);
		pluss(i,x-pre);
		pre=x;
	}
	while(m--){
		scanf("%lld",&opt);
		if(opt==1){
			scanf("%lld%lld%lld",&x,&y,&z);
			pluss(x,z);
			pluss(y+1,-z);
		}else{
			scanf("%lld%lld",&x,&y);
			printf("%lld\n",search(y)-search(x-1));
		}
	}
	return 0;
}

注:两数据结构在模板题\(P3372\)的时空比较

带有\(lazytag\)懒标优化的线段树:\(378ms\ 12.98MB\)
上述区间修改与查询的树状数组:\(163ms\ 7.00MB\)

完结撒花!

posted @ 2021-10-31 16:31  qzhwlzy  阅读(62)  评论(0)    收藏  举报