树状数组从入门到弃疗

树状数组是一类存储后缀和,更新后缀和,通过lowbit来限定后缀和的长度,利用二进制使得查询、更新的时间复杂度都在\(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

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

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

我是\(SB\)

/*
@ 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

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

对于差分数组\(delta\)

差分数组的前缀和为\(val_i=\sum\limits_{j=1}^idelta_j\)

对于区间\([l,r]\)

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

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

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

\(s_p=\sum\limits_{i=1}^p\sum\limits_{j=1}^idelta_j\)

\(s_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\)

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

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

但是对于后者而言,求和式中每一项只与\(i\)有关,可以通过一次容斥,把\((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 上帝造题的七分钟

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

\(\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}\)

可以看出只需要分别用\(4\)个树状数组维护\(d[i][j]\),\(j*d[i][j]\),\(d[i][j]*i\),\(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[1]...a[n]\)中等于\(i\)的个数

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[1]...a[n]\)中小于等于\(i\)的元素个数

操作

求排名

求小于等于\(v\)的元素的数目

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

添加元素

将值为v的元素增加\(num\)

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

求第k大

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

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

\(2\le N\le8000\)

如果最后一头牛前面有\(a_n\)头牛比它矮,那它的\(h_n=a_n+1\)

对于倒数第二头牛

  • \(a_{n-1}<a_n\)\(h_{n-1}=a_{n-1}+1\)
  • \(a_{n-1}\ge a_n\)\(h_{n-1}=a_{n-1}+2\)

对于第\(k\)头牛,如果前面有\(a_k\)头牛比它矮,\(h_k\)就是数值\(1\)\(n\)中第\(a_k+1\)小的没有在\(\{h_{k+1},h_{k+2}...h_{n}\}\)出现过的数

所以我们需要维护一个\(01\)序列,支持查询第\(k\)\(1\)的位置,而且支持修改序列中的值

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

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

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

不会,复杂度\(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]异或和

给定一个长为\(n\)的序列\(a\),求这个序列所有的连续和的异或值

\(1\le n\le 10^5\)

  • \(20pts\)

枚举起点,枚举长度即可

  • \(100pts\)

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

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

\(s[i]\)表示\(a\)的前缀和

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

若数量为奇数,则相当于异或\(1\) ,只需记录这一位异或值的变量\(cnt\)异或\(1\)

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

那么该如何统计答案呢?

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

现在问题在于如何求\(cnt\)

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

可以构造两棵权值树状数组,一棵记录当前位为\(1\)的,另一棵记录为\(0\)

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

如果第\(k\)位为\(0\)的话,如果后面再比\(s[i]\)大的话,\(s[i]\)\(k\)位的\(1\)就需要借给低一位的了,所以后面必须不比\(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 :区间修改,区间查询

权值树状数组的简单介绍

posted @ 2020-06-24 22:14  pyyyyyy  阅读(315)  评论(0编辑  收藏  举报