树状数组从入门到弃疗

[TOC]

树状数组是一类存储后缀和,更新后缀和,通过lowbit来限定后缀和的长度,利用二进制使得查询、更新的时间复杂度都在O(logn)O(logn)的数据结构,码量十分小,常数优秀

注意:以下下代码部分未经过压力测试,不保证完全正确

一维树状数组

单点修改+区间查询

树状数组 1

#include<bits/stdc++.h>
#define int long long 
using namespace std;
const int N=1e6+2020;
int c[N],a[N],n,q;
int lowbit(int x) {
	return x&-x;
}
int sum(int r) {
	int ret=0;
	while(r>0) ret+=c[r],r-=lowbit(r);
	return ret;
}
void add(int x,int val) {
	while(x<=n) c[x]+=val,x+=lowbit(x);
}
signed main() {
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin>>n>>q;
	for(int i=1; i<=n; ++i) 
		scanf("%lld",&a[i]),add(i,a[i]);
	for(int i=1; i<=q; ++i) {
		int opt,val,x;
		scanf("%lld %lld %lld",&opt,&x,&val);
		if(opt==1) a[x]+=val,add(x,val);
        else if(opt==2) cout<<sum(val)-sum(x-1)<<'\n';
	}
	return 0;
}

区间修改,单点查询

树状数组 2

利用差分数组还原原数组的方法即可实现单点查询

显然差分数组可以快速修改区间

我是SBSB

/*
@ author:pyyyyyy/guhl37
-----思路------

-----debug-------

*/
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e6+2020;
int delta[N],n,q,a[N];
int lowbit(int x) {
	return x&(-x);
}
void add(int x,int val) {
	while(x<=n) delta[x]+=val,x+=lowbit(x);
}
int sum(int r) {
	int ret=0;
	while(r) ret+=delta[r],r-=lowbit(r);
	return ret;
}
signed main() {
	cin>>n>>q;
	for(int i=1; i<=n; ++i)
		scanf("%lld",&a[i]);
	while(q--) {
		int opt,l,r,x;
		scanf("%lld",&opt);
		if(opt==1) scanf("%lld %lld %lld",&l,&r,&x),add(l,x),add(r+1,-x);
		else if(opt==2) scanf("%lld",&x),cout<<a[x]+sum(x)<<'\n';
	}
	return 0;
}

区间修改+区间查询

树状数组 3

对一个差分数组做一次前缀和可以得到每个位置的值再对每个位置累加一下就是一个区间的值

对于差分数组deltadelta

差分数组的前缀和为vali=j=1ideltajval_i=\sum\limits_{j=1}^idelta_j

对于区间[l,r][l,r]

sl,r=i=1rvalii=1l1valis_{l,r}=\sum\limits_{i=1}^rval_i-\sum\limits_{i=1}^{l-1}val_i(前缀和相减的形式)

可以发现,一个区间的值实际上就是差分数组前缀和的前缀和做减法

我们可以用树状数组维护差分数组前缀和的前缀和

sp=i=1pj=1ideltajs_p=\sum\limits_{i=1}^p\sum\limits_{j=1}^idelta_j

sp=i=1p(pi+1)ci=(p+1)i=1pcii=1picis_p=\sum\limits_{i=1}^p\left(p-i+1\right)c_i=\left(p+1\right)\sum\limits_{i=1}^pc_i-\sum\limits_{i=1}^pi *c_i

注意为什么我们要把(pi+1)(p-i+1)拆掉,这里利用了利用分离包含多个变量的项,使公式中不同变量之间相互独立的思想

对于前者来说,求和式子中每一项都包含(pi+1)(p-i+1),在修改是我们无法确定(pi+1)(p-i+1)的值,只能维护cic_i的前缀和和。在询问时会出现系数为等差数列的求和式,这不是我们想看到的

但是对于后者而言,求和式中每一项只与ii有关,可以通过一次容斥,把(p+1)(p+1)巧妙地变成常数,这就是上面思想的应用

显然这些东西都可用树状数组维护一下

/*
@ author:pyyyyyy/guhl37
-----思路------

-----debug-------
add里面为什么条件是<=N?
*/
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2000005;
int n,q;
int lowbit(int x) {
	return x&-x;
}
void add(int *arr,int x,int val) {
	while(x<=N) arr[x]+=val,x+=lowbit(x);
	//这是为什么是x<=N? 
}
int sum(int *arr,int x) {
	int ret=0;
	while(x) ret+=arr[x],x-=lowbit(x);
	return ret;
}
int a[N],d[N],id[N];
int ans(int k) {
	return k*sum(d,k)-sum(id,k);
}
signed main() {
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin>>n>>q;
	for(int i=1; i<=n; ++i) {
		scanf("%lld",&a[i]);
		add(d,i,a[i]-a[i-1]);
		add(id,i,(i-1)*(a[i]-a[i-1]));
	}
	while(q--) {
		int opt,l,r,x;
		cin>>opt;
		if(opt==1) {
			scanf("%lld %lld %lld",&l,&r,&x);
			add(d,l,x),add(d,r+1,-x);
			add(id,l,(l-1)*x),add(id,r+1,-r*x);
		} else if(opt==2) {
			scanf("%lld %lld",&l,&r);
			cout<<ans(r)-ans(l-1)<<'\n';
		}
	}
	return 0;
}

上面有个不太懂的地方,恳请大佬解答

区间最值

没想到树状数组能干这个 ,其实常数也蛮大的了,没什么意义,还不如写线段树

void build(int n){
	for(int i=1;i<=n;++i)
	{
		c[i]=a[i];int t=lowbit(i);
		for(int j=1;j<t;j*=2) c[i]=max(c[i],c[i-j]);
	}
}
void add(int pos,int x)
{
	a[pos]=x;
	while(pos<=n){
		c[pos]=a[pos];int t=lowbit(i);
		for(int j=1;j<t;j*=2) c[i]=max(c[i],c[i-j]);
		pos+=lowbit(pos);
	}
}
int query(int l,int r)
{
	int ans=a[r];
	while(1)
	{
		ans=max(ans,num[r]);
		if(r==1) break;
		r--;
		while(r-l>=lowbit(r)) ans=max(ans,c[r]),r-=lowbit(r);
	}
	return ans;
}

二维树状数组

单点修改,区间查询

二维树状数组 1

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5096;
int c[N][N],n,m;
int lowbit(int x) {
	return x&-x;
}
void add(int x,int y,int val) {
	int t=y;
	while(x<=n) {
		y=t;
		while(y<=m) c[x][y]+=val,y+=lowbit(y);
		x+=lowbit(x);
	}
}
int sum(int x,int y) {
	int ret=0,t=y;
	while(x) {
		y=t;
		while(y) ret+=c[x][y],y-=lowbit(y);
		x-=lowbit(x);
	}
	return ret;
}
signed main() {
	cin>>n>>m;
	int opt;
	while((scanf("%lld",&opt))==1) {
		if(opt==1) {
			int x,y,k;
			scanf("%lld %lld %lld",&x,&y,&k);
			add(x,y,k);
		} else if(opt==2) {
			int ax,ay,bx,by;
			scanf("%lld %lld %lld %lld",&ax,&ay,&bx,&by);
			cout<<sum(bx,by)-sum(ax-1,by)-sum(bx,ay-1)+sum(ax-1,ay-1)<<'\n';
		}
	}
	return 0;
}

区间修改,单点查询

二维树状数组 2

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=(1<<12)+20;
int n,m,c[N][N],a[N][N];
int lowbit(int x) {
	return x&-x;
}
void add(int x,int y,int val) {
	int t=y;
	while(x<=n) {
		y=t;
		while(y<=m) c[x][y]+=val,y+=lowbit(y);
		x+=lowbit(x);
	}
}
int sum(int x,int y) {
	int ret=0,t=y;
	while(x) {
		y=t;
		while(y) ret+=c[x][y],y-=lowbit(y);
		x-=lowbit(x);
	}
	return ret;
}
signed main() {
	cin>>n>>m;
	int opt,ax,ay,bx,by,val;
	while((scanf("%lld",&opt))==1) {
		if (opt == 1) {
			scanf("%lld %lld %lld %lld %lld",&ax,&ay,&bx,&by,&val);
			add(ax,ay,val),add(ax,by+1,-val);
			add(bx+1,by+1,val),add(bx+1,ay,-val);
		} else {
			scanf("%lld%lld",&ax,&bx);
			printf("%lld\n",sum(ax,bx));
		}
	}
	return 0;
}

区间修改,区间查询

二维树状数组 3

P4514 上帝造题的七分钟

同样利用分离包含多个变量的项,使公式中不同变量之间相互独立的思想

sum[x][y]=i=1xj=1ya[i][j]=i=1xj=1y(p=1iq=1jd[p][q])=i=1xj=1yp=1i((y+1)d[p][j]d[p][j]j)=i=1xj=1y(x+1)((y+1)d[i][j]d[i][j]j)((y+1)d[i][j]d[i][j]j)i=i=1xj=1y(x+1)(y+1)d[i][j](x+1)jd[i][j](y+1)id[i][j]+ijd[i][j]=(x+1)(y+1)i=1xj=1yd[i][j](x+1)i=1xj=1yjd[i][j](y+1)i=1xj=1yid[i][j]+i=1xj=1yijd[i][j]\begin{aligned} & \operatorname{sum}[x][y]=\sum_{i=1}^{x} \sum_{j=1}^{y} a[i][j]=\sum_{i=1}^{x} \sum_{j=1}^{y}\left(\sum_{p=1}^{i} \sum_{q=1}^{j} d[p][q]\right) \\ =& \sum_{i=1}^{x} \sum_{j=1}^{y} \sum_{p=1}^{i}((y+1) * d[p][j]-d[p][j] * j) \\ =& \sum_{i=1}^{x} \sum_{j=1}^{y}(x+1) *((y+1) * d[i][j]-d[i][j] * j)-((y+1) * d[i][j]-d[i][j] * j) * i \\ =& \sum_{i=1}^{x} \sum_{j=1}^{y}(x+1) *(y+1) * d[i][j]-(x+1) * j * d[i][j]-(y+1) * i * d[i][j]+i * j * d[i][j] \\ =&(x+1) *(y+1) \sum_{i=1}^{x} \sum_{j=1}^{y} d[i][j]-(x+1) \sum_{i=1}^{x} \sum_{j=1}^{y} j * d[i][j]-(y+1) \sum_{i=1}^{x} \sum_{j=1}^{y} i * d[i][j]+\\ & \sum_{i=1}^{x} \sum_{j=1}^{y} i * j * d[i][j] \end{aligned}

可以看出只需要分别用44个树状数组维护d[i][j]d[i][j],jd[i][j]j*d[i][j],d[i][j]id[i][j]*i,ijd[i][j]i*j*d[i][j]即可

/*
P4514 上帝造题的七分钟
@ author:pyyyyyy/guhl37
-----思路------

-----debug-------

*/
#include<bits/stdc++.h>
using namespace std;
//#define int long long 
const int N=2050;
int n,m,c1[N][N],c2[N][N],c3[N][N],c4[N][N];
int lowbit(int x) {
	return x&-x;
}
void add(int x,int y,int val) {
	for(int i=x; i<=n; i+=lowbit(i))
		for(int j=y; j<=m; j+=lowbit(j)) {
			c1[i][j]+=val;
			c2[i][j]+=val*x;
			c3[i][j]+=val*y;
			c4[i][j]+=val*x*y;
		}
}
int sum(int x,int y) {
	int ret=0;
	for(int i=x; i>0; i-=lowbit(i))
		for(int j=y; j>0; j-=lowbit(j))
			ret+=(x+1)*(y+1)*c1[i][j]-(y+1)*c2[i][j]-(x+1)*c3[i][j]+c4[i][j];
	return ret;
}
char opt;
signed main() {
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin>>opt>>n>>m;
	while(cin>>opt) {
		if(opt=='L') {
			int ax,ay,bx,by,val;
			scanf("%d %d %d %d %d",&ax,&ay,&bx,&by,&val);
			add(ax,ay,val),add(bx+1,by+1,val);
			add(ax,by+1,-val),add(bx+1,ay,-val);
		} else if(opt=='k') {
			int ax,ay,bx,by;
			scanf("%d %d %d %d",&ax,&ay,&bx,&by);
			printf("%d\n",sum(bx,by)-sum(bx,ay-1)-sum(ax-1,by)+sum(ax-1,ay-1));
		}
	}
	return 0;
}
PS:此代码没法AC,因为这题需要快读,我懒得写了

权值树状数组

权值数组

A[i]A[i]表示序列a[1]...a[n]a[1]...a[n]中等于ii的个数

for(int i=1;i<=n;++i)
    ++A[a[i]];

权值数组的前缀和

for(int i=minval+1;i<=maxval;++i) 
    A[i]+=A[i-1];

权值数组的前缀和A[i]A[i]就表示原序列a[1]...a[n]a[1]...a[n]中小于等于ii的元素个数

操作

求排名

求小于等于vv的元素的数目

int getsum(int val){
	int ret=0;
    for(;val;val-=lowbit(val))
		ret+=cnt[val];
    return ret;
}

添加元素

将值为v的元素增加numnum

int add(int val,int num)
{
	for(;val;val+=lowbit(val))
        cnt[val]+=num;
}

求第k大

权值数组前缀和是单调递增的,那么权值树状数组自然也是单调递增的,利用这一点我们可以二分查询原序列中第k大的值。拿getsum(mid)getsum(mid)的值跟k值相比来缩小上下界就可以做到这一点。时间复杂度O((lgn)2)O((lgn)^2)

while(l<=r)
{
    int mid=(l+r)>>1;
   	int t=getsum(mid);
    if(t<k) l=mid+1;
    else r=mid-1;
}

Lost Cows

NN头奶牛排队,它们的身高为1n1\backsim n,知道每头牛前面有多少头比自己矮,求每头牛的身高。

2N80002\le N\le8000

如果最后一头牛前面有ana_n头牛比它矮,那它的hn=an+1h_n=a_n+1

对于倒数第二头牛

  • an1<ana_{n-1}<a_nhn1=an1+1h_{n-1}=a_{n-1}+1
  • an1ana_{n-1}\ge a_nhn1=an1+2h_{n-1}=a_{n-1}+2

对于第kk头牛,如果前面有aka_k头牛比它矮,hkh_k就是数值11nn中第ak+1a_k+1小的没有在{hk+1,hk+2...hn}\{h_{k+1},h_{k+2}...h_{n}\}出现过的数

所以我们需要维护一个0101序列,支持查询第kk11的位置,而且支持修改序列中的值

  • 方法一:树状数组+二分

不再赘述,见上面。复杂度O(log2n)O(log^2n)

  • 方法二:树状数组+倍增

不会,复杂度O(logn)O(logn)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+2020;
int c[N],a[N],n,ans[N];
int lowbit(int x)
{
	return x&-x;
}
int add(int x,int val)
{
	for(;x<=n;x+=lowbit(x)) c[x]+=val;
}
int sum(int x)
{
	int ret=0;
	for(;x;x-=lowbit(x)) ret+=c[x];
	return ret;
}
int find(int k)
{
	int mid,l=1,r=n,ret;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(sum(mid)>=k) r=mid-1,ret=mid; 
		else l=mid+1;
	}
	return ret;
}
int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin>>n;
	add(1,1);
	for(int i=2;i<=n;++i) 
	{
		scanf("%d",&a[i]);
		add(i,1);
	}
	for(int i=n;i>=1;--i)
	{
		int pos=find(a[i]+1);
		ans[i]=pos;
		add(pos,-1);
	}
	for(int i=1;i<=n;++i) cout<<ans[i]<<'\n';
	return 0;
}

例题

P3760 [TJOI2017]异或和

给定一个长为nn的序列aa,求这个序列所有的连续和的异或值

1n1051\le n\le 10^5

  • 20pts20pts

枚举起点,枚举长度即可

  • 100pts100pts

位运算每个位的结果这和这一位的数有关

我们考虑计算每一位对答案的贡献。

s[i]s[i]表示aa的前缀和

假设我们现在算到最右位202^0,并且位于第ii个数,想要知道以ii结尾的连续和对答案的贡献,只需要知道有多少s[i]s[j]0jis[i]-s[j],0\le j \le i202^0位是11

若数量为奇数,则相当于异或11 ,只需记录这一位异或值的变量cntcnt异或11

若为偶数,则相当于没有异或,不变即可

那么该如何统计答案呢?

对于数的每一位如果最后cnt=1cnt=1的话,就说明在这一位所有连续和的异或和为11,我们就需要把答案加上22^{位数}

现在问题在于如何求cntcnt

注意数据范围中i=1nai106\sum\limits_{i=1}^na_i\le10^6

可以构造两棵权值树状数组,一棵记录当前位为11的,另一棵记录为00

如果当前扫描到的s[i]s[i]的二进制第kk位为11,那么对这一位的答案有贡献的只有那些第kk位为11且第kk位向右的数比s[i]s[i]kk位向右的数大的或者第kk位为00且第k位向右的数不比s[i]s[i]kk位向右的数大的。

如果第kk位为00的话,如果后面再比s[i]s[i]大的话,s[i]s[i]kk位的11就需要借给低一位的了,所以后面必须不比s[i]s[i]大。

上面这里具体如何处理请看处理得很妙的一篇题解

#include<bits/stdc++.h>
#define int long long
using namespace std;
int s[100005],a[100005];
int f[2][1000005],n,m,ans,now,cnt,tmp;
int flag;
int maxn=-999;
int lowbit(int x) {
	return x&-x;
}
int update(int x,int y) {
	for(; x<=1000000; x+=lowbit(x)) f[y][x]++;
}
int sum(int x,int y) {
	int ret=0;
	for(; x; x-=lowbit(x)) ret+=f[y][x];
	return ret;
}
signed main() {
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	cin>>n;
	for(int i=1; i<=n; ++i) {
		scanf("%lld",&s[i]);
		s[i]+=s[i-1];
		maxn=max(s[i],maxn);
	}
	for(int i=0; i<=20; ++i) {
		if((1<<i)>maxn) break;
		memset(f,0,sizeof(f));
		flag=0,cnt=0;
		update(1,0);
		for(int j=1; j<=n; ++j) {
			tmp=s[j]&(1<<i);
			if(tmp) now=sum(a[j]+1,0)+sum(1000000,1)-sum(a[j]+1,1);
			else now=sum(a[j]+1,1)+sum(1000000,0)-sum(a[j]+1,0);
			if(now%2) cnt^=1;
			update(a[j]+1,(tmp>0?1:0));
			a[j]|=tmp;
		}
		if(cnt) ans+=(1<<i);
	}
	cout<<ans;
	return 0;
}

参考资料

树状数组简单易懂的详解

可以代替线段树的树状数组?——树状数组进阶

树状数组的区间修改,区间查询

树状数组 3 :区间修改,区间查询

权值树状数组的简单介绍