树状数组总结

可以支持 Θ(logn)\Theta(\log n) 单点修改,Θ(logn)\Theta(\log n) 查询的数据结构。一个挺有用的数据结构。原理请见 Oi-Wiki

基本的代码:

int c[N];
int lowbit(int x) {
  // x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
    return x & -x;
}
int sum(int x) {  // a[1]..a[x]的和
    int p = 0;
    for(;x;x-=lowbit(x)) p+=c[x];
    return ans;
}
void upd(int x, int k) {
  	for(;x<=n;x+=lowbit(x) c[x]+=k;
}

区间修改,单点查询

其实就是差分的前缀和。把差分的好的数据丢进去,再进行修改,得到的和就是原数组经过修改的值。也可以这样,将原数列设为 00。将修改后的数组加上 aia_i 就好了。

#include<bits/stdc++.h>
using namespace std;
int n,m,o,x,y,k,a[500005],c[500005];
int lowbit(x) {
	return x&-x;
}
void update(int x,int y) {
	for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int check(int x) {
	int p=0;
	for(;x;x-=lowbit(x)) p+=c[x];
	return p;
}
int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	while(m--) {
		scanf("%d",&o);
		if(o==1) {
			scanf("%d%d%d",&x,&y,&k);
			update(x,k);
			update(y+1,-k);
		}
		else {
			scanf("%d",&x);
			printf("%d\n",a[x]+check(x));
		}
	}
	return 0;
}

区间修改,区间查询

对于区间修改,非常熟悉,直接定义差分数组 di=aiai1d_i=a_i-a_{i-1}。便有 ai=j=1idia_i=\sum\limits_{j=1}^{i} d_i。设 si=j=1iajs_i=\sum\limits_{j=1}^i a_j,带入上式得:si=j=1ik=1jdks_i=\sum\limits_{j=1}^i\sum\limits_{k=1}^j d_k。观察该式,可得每个 dkd_k 加了 ik+1i-k+1 次,即:

si=k=1idk×(ik+1)=k=1idk×(i+1)k=1idk×k\begin{aligned} s_i&=\sum\limits_{k=1}^{i}d_k\times(i-k+1)\\ &=\sum\limits_{k=1}^{i}d_k\times(i+1)-\sum\limits_{k=1}^{i}d_k\times k \end{aligned}

现在就可以使用两个树状数组 c1c1c2c2,分别维护 dk,dk×kd_k,d_k\times k 的前缀和(i+1i+1 是固定的,查询时再乘上)。

如何应用

现在要在 alara_l \sim a_r 中,全部加 kk。在 dd 中就是在 dl+k,dr+1kd_l+k,d_{r+1}-k。放到 c1,c2c1,c2 中分别是:

  • c1l+k,c1r+1kc1_l+k,c1_{r+1}-k.
  • c2l+k×l,c2r+1k×(r+1)c2_l+k\times l,c2_{r+1}-k\times(r+1).

代码

// 适用 P3372 【模板】线段树 1
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6;
int c[2][N],a[N],n,m,op,x,y,k;
int lowbit(int x) {
	return x& -x;
}
void add(int p,int v) {
	for(int i=p;i<=n;i+=lowbit(i)) {
		c[0][i]+=v,c[1][i]+=v*p;//这里不是 i,i会改变
	}
}
int query(int p) {
	int res=0;
	for(int i=p;i;i-=lowbit(i)) {
		res+=c[0][i]*(p+1)-c[1][i];
	}
	return res;
}
signed main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		cin>>a[i];
		add(i,a[i]-a[i-1]);
	}
	for(int i=1;i<=m;i++) {
		cin>>op>>x>>y;
		if(op==1) {
			cin>>k;
			add(x,k);
			add(y+1,-k);
		}
		else {
			cout<<query(y)-query(x-1)<<"\n";
		}
	}
	return 0;
}

二维树状数组

单点修改,矩阵查询

对于二维的树状数组,我们设 cx,yc_{x,y} 为左上角为 xlowbit(x)+1,ylowbit(y)+1{x-\operatorname{lowbit}(x)+1,y-\operatorname{lowbit}(y)}+1,右下角为 x,yx,y 的矩阵。先行后列,我们就可以理解为嵌套的树状数组。
对于单点修改,需要将所有包含 x,yx,yci,jc_{i,j} 都修改一遍。
对于查询,使用类似二维前缀和的方法。

void upd(int x,int y,int k) {//将 a[x][y] 加 k 
	for(int i=x;i<=n;i+=lowbit(i)) {
		for(int j=y;j<=n;j+=lowbit(j)) {
			c[i][j]+=k;
		}
	}
}
int s(int xf,int yf,int xl,int yl) {//求 a[xf][yf]~a[xl][yl]的和 
	return ss(xl,yl)-ss(xf-1,yl)-ss(xl,yf-1)+ss(xf-1,yf-1);
}
int ss(int x,int y) {
	int p=0;
	for(int i=n;i>=x;i-=lowbit(i)) {
		for(int j=n;j>=y;j-=lowbit(j)) {
			p+=c[i][j];
		}
	}
	return p;
}

矩阵修改,矩阵查询

基于一维数组的区间修改,区间查询。我们同样也可以在二维数组中差分,为:di,j=ai,jai1,jai,j1+ai1,j1d_{i,j}=a_{i,j}-a_{i-1,j}-a_{i,j-1}+a_{i-1,j-1}

现在要查询左上角为 1,11,1,右下角为 x,yx,y 的矩阵和可以表示为:

i=1xj=1yai,j=i=1xj=1yk=1il=1jdk,l\begin{aligned}&\sum_{i=1}^x\sum_{j=1}^ya_{i,j}\\ =&\sum_{i=1}^x\sum_{j=1}^y\sum_{k=1}^i\sum_{l=1}^jd_{k,l}\end{aligned}

考虑化简这个式子。对于 dk,ld_{k,l},当 ik&jli\ge k\And j\ge l 时,他就会被累加一次。iixk+1x-k+1 个值,jjyl+1y-l+1 个取值。乘法原理一用,得:(以下的 i,ji,j 为原来的 k,lk,l

i=1xj=1ydi,j×(xi+1)×(yj+1)\sum_{i=1}^x\sum_{j=1}^y d_{i,j}\times(x-i+1)\times(y-j+1)

将不会变的 x,yx,y 和会变的 i,ji,j 分开:

i=1xj=1ydi,j×(xy+x+y+1)di,j×i×(y+1)di,j×j×(x+1)+di,j×i×j\sum_{i=1}^x\sum_{j=1}^y d_{i,j}\times(xy+x+y+1)-d_{i,j}\times i\times(y+1)-d_{i,j}\times j\times (x+1)+d_{i,j}\times i\times j

按照一样的思想,把定值放在最后处理。维护  di,j , di,j×i , di,j×j , di,j×i×j \ d_{i,j}\ ,\ d_{i,j}\times i\ ,\ d_{i,j}\times j\ ,\ d_{i,j}\times i\times j\

实现:

typedef long long ll;
ll t1[N][N],t2[N][N],t3[N][N],t4[N][N];
void add(llx,lly,ll z) {
	for(int X=x;X<=n;X+=lowbit(X)){
		for(int Y=y;Y<=m;Y+=lowbit(Y)){
			t1[X][Y]+=z;
			t2[X][Y]+=z*x;//注意是z*x而不是z*X,后面同理
			t3[X][Y]+=z*y;
			t4[X][Y]+=z*x*y;
		}
	}
}
void range_add(ll xa,ll ya,ll xb,ll yb,ll z){//(xa,ya)到(xb,yb)子矩阵
	add(xa,ya,z);
	add(xa,yb+1,-z);
	add(xb+1,ya,-z);
	add(xb+1,yb+1,z);
}
ll ask(ll x,ll y){
	ll res=0;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			res+=(x+1)*(y+1)*t1[i][j]-(y+1)*t2[i][j]-(x+1)*t3[i][j]+t4[i][j];
	return res;
}
ll range_ask(ll xa,ll ya,ll xb,ll yb){
	return ask(xb,yb)-ask(xb,ya-1)-ask(xa-1,yb)+ask(xa-1,ya-1);
}

权值树状数组

对一个序列的权值数组 bb 构建树状数组。

权值数组

序列每个数出现的次数,类似于数组计数。如 1 1 4 5 1 4,权值数组就是 3 0 0 2 1

小技巧

当值域很大,并且关注数的大小关系时,可以离散化。


问题 1

修改操作:要将 aia_ixx 修改为 yy,其实就是 bx1b_x-1by+1b_y+1

我们可以用这种方法解决单点修改,全局 kk 小值问题(具体可见LOG,本文不探讨此题的实现)。

考虑如何查询 kk 小值,如果用前缀和,每次二分查询 cx<kc_x<kcx+1kc_{x+1}\ge kcc 是前缀和)那么查询是 Θ(logn)\Theta(\log n) 。但是修改要对权值数组重新前缀和,在线处理特别慢。但是树状数组可以实现前缀和的功能。这样的复杂度就是 Θ(log2n)\Theta(\log^2 n)。虽然实现了加速,但还有优化的空间。


此时就要考虑到树状数组美好的性质,查询 [1,x][1,x] 的树状数组是 Θ(logn)\Theta(\log n) 的,但是某些值只需要查询一次就好了。比如我们要查询 [xlowbit(x)+1,x][x-\operatorname{lowbit}(x)+1,x],只需要访问 cxc_x 就好了。

那现在有 [x+1,x+2i][x+1,x+2^i]。如果 lowbit(x+2i)=2i\operatorname{lowbit}(x+2^i)=2^i,那这个区间就是 cx+2ic_{x+2^i}。考虑如何满足这样的性质。将 xx 分为若干个 2i2^i,很符合我们的倍增思想!

以下设 ss 为当前区间内数的数量,xx 是当前区间末尾,从大到小枚举 2i2^i

  • 如果满足 s+2i<ks+2^i<k,将 xx+2ix\gets x+2^iss+cx+2is\gets s+c_{x+2^i}
  • 如果不满足,就尝试更小的 ii

模拟1 1 1 1 1 1 1,要找第 77 小数。
开始 s=x=0s=x=0
找到 222^2 满足,s=4,x=4s=4,x=4。(s+c4s+c_4c4c_4 管辖 [1,4][1,4]
找到 212^1 满足,s=6,x=6s=6,x=6。(s+c6s+c_6c6c_6 管辖 [5,6][5,6]

会发现,由于倍增从大到小,在二进制中也就越后。所以新加的区间一定满足 lowbit(x+2i)=2i\operatorname{lowbit}(x+2^i)=2^i。复杂度得到降低:Θ(log2n)Θ(logn)\Theta(\log^2 n)\to \Theta(\log n)

Code
int find(int k) {
	int x=0,sum=0;
	for(int i=__lg(len);~i;i--)
	//__lg 就是 log2,但是时间复杂度为Θ(1) 
		if(x+(1<<i)<len&&sum+c[x+(1<<i)]<k)
		//这里不能越界 
			x+=1<<i,sum+=c[x];
	//最后要再 +1 可参考倍增 LCA 
	return x+1;
}

了解了一些基础的理论知识,让我们来看道题吧。

【模板】普通平衡树

题意如下: 你要维护数据结构,支持以下总次数为 nn 的 6Θ\Theta 种操作:

  1. 插入一个数 xx
  2. 删除一个数 xx
  3. 查询 xx 的排名(排名为比 xx 小的数个数 +1+1
  4. 查询排名为 xx 的数
  5. 查询比 xx 小的最大的数
  6. 查询比 xx 大的最小的数

对于 100%100\% 的数据,1n105,x1071\le n\le 10^5,|x|\le 10^7

呦呦呦,这不平衡树吗

其实树状数组可以用很短的代码(不到 1KB)解决本题。首先离散化下。

1,2 用权值树状数组秒杀。
第 3 个只要查询 <x<x 的数的数量,其实就是求 x1x-1 的前缀和, +1+1
4 不就是求 kk 小值么。
5,6 差不多。但实现上有一点区别。考虑转化成问题 3,4。

  • 对于 5。先求比 xx 小的数的排名(记作 kk),就变成查询 kk 小值。

  • 对于 6。求比 xx 大的数的排名(记作 kk)。也变成查询 kk 小值。


时间复杂度 Θ(nlogn)\Theta(n\log n)。并且常数小,码量小。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],b[N],rk[N],c[N],cnt,len;
int lowbit(int x)
{
	return x&-x;
}
int wh(int x)
{
	return lower_bound(rk+1,rk+len+1,x)-rk;
}
void upd(int x,int z)
{
	for(;x<=len;x+=lowbit(x)) c[x]+=z;
} 
int s(int x)
{
	int p=0;
	for(;x;x-=lowbit(x))
		p+=c[x];
	return p;
}
int find(int k) {
	int x=0,sum=0;
	for(int i=__lg(len);~i;i--)
		if(x+(1<<i)<len&&sum+c[x+(1<<i)]<k)
			x+=1<<i,sum+=c[x];
	return x+1;
}
int main() {
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i]>>b[i];
		if(a[i]!=4) rk[++cnt]=b[i];
	}
	sort(rk+1,rk+cnt+1);
	len=unique(rk+1,rk+cnt+1)-(rk+1);
	//对权值离散化,以下提到的权值数组均为离散化后的权值 
	for(int i=1;i<=n;i++)
	{
		switch(a[i])
		{
			//wh 为权值在权值数组中的下标 
			case 1:upd(wh(b[i]),1);break;
			case 2:upd(wh(b[i]),-1);break;
			//离散化后的权值是连续的,所以 wh(b[i])-1 就是第一个 <x 的数 
			case 3:cout<<s(wh(b[i])-1)+1<<"\n";break;
			//查询 k 小值 
			case 4:cout<<rk[find(b[i])]<<"\n";break;
			//s(wh(b[i])-1) 为第一个 <x 的数的数量。然后查询 s(wh(b[i])-1) 小值就好了 
			case 5:cout<<rk[find(s(wh(b[i])-1))]<<"\n";break;
			//s(wh(b[i])) 为 ≤x 的数的数量,+1 后为比 x 大的数的排名。查询这个排名的值就好了 
			case 6:cout<<rk[find(s(wh(b[i]))+1)]<<"\n";break;
			/*
				rk[i] 就是将权值数组变回实际权值
			*/ 
		}
	}
	return 0;
}

问题 2

一点应用

逆序对,就是指在一个序列中 i<j,ai>aji<j,a_i>a_j 的数。很多时候使用归并排序解决。其实树状数组也可以。步骤如下:

  1. 离散化一下。
  2. 用树状数组记录权值为 ii 的个数。对于 aia_i,逆序对为a1i1>aia_{1\sim i-1}>a_i 的个数。即 slensais_{len}-s_{a_{i}}lenlen为离散化后个数,也是最大的权值)。
  3. 求完后再将 aia_i 加入树状数组。

#include<bits/stdc++.h>
using namespace std;
#define int long long
int t[500005],a[500005],rk[500005],s[500005],n,ans;
int lowbit(int x) {
	return x & -x;
}
void upd(int i,int k) {
	for(;i<=n;i+=lowbit(i)) t[i]+=k;
}
int ss(int i) {
	int p=0;
	for(;i;i-=lowbit(i)) p+=t[i];
	return p;
}
signed main() {
	scanf("%lld",&n);
	for(int i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
		rk[i]=a[i];
	}
	sort(rk+1,rk+n+1);
	int len=unique(rk+1,rk+n+1)-(rk+1);
	for(int i=1;i<=n;i++)
		s[i]=lower_bound(rk+1,rk+len+1,a[i])-rk;
	for(int i=n;i>=1;i--) {
		ans+=ss(s[i]-1);
		upd(s[i],1);
	}
	cout<<ans;
	return 0;
}

小技巧

建树

可以把每个元素都当成单点修改,复杂度 Θ(nlogn)\Theta(n\log n)
但……仅仅是这样吗?

以下提供两种 Θ(n)\Theta(n) 建树的方法。

第 1 种

对于 cxc_x,他的父亲节点是 cx+lowbit(x)c_x+\operatorname{lowbit}(x)。而父亲节点是子节点的和。只需要用所有儿子更新父亲节点。

// Θ(n) 建树
void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] += a[i];
    int j = i + lowbit(i);
    if (j <= n) t[j] += t[i];
  }
}
第 2 种

树状数组的性质是这样的:第 cic_i 管理的区间是 [ilowbit(i)][i-\operatorname{lowbit}(i)]
要是我们知道数组任意区间的和就好了。所以,直接用前缀和。

void init() {
  for (int i = 1; i <= n; ++i) {
    t[i] = sum[i] - sum[i - lowbit(i)];
  }
}

神奇的思路...


时间戳

若有多测,数组清零可能超时。

此时可用数组记录上次使用的时间,如果相同,值可用。不同说明实际的值为 00

int tag[MAXN], t[MAXN], Tag;
void reset() { ++Tag; }
void add(int k, int v) {
  while (k <= n) {
    if (tag[k] != Tag) t[k] = 0;
    t[k] += v, tag[k] = Tag;
    k += lowbit(k);
  }
}
int getsum(int k) {
   int ret = 0;
   while (k) {
     if (tag[k] == Tag) ret += t[k];
     k -= lowbit(k);
  }
  return ret;
}

模板

以下是用结构体封装模板。

struct BIT{
	int c[N],t[N];
	int tg,len=N;
	void clear() {tg++;}
	void init(int l) {len=l;}
	#define lowbit(x) ((x)&-(x))
	void upd(int x,int k) {for(;x<=len;x+=lowbit(x)) if(tg==t[x]) c[x]+=k; else c[x]=k, t[x]=tg;}
	int s(int x) {int p=0;for(;x;x-=lowbit(x)) if(tg==t[x]) p+=c[x]; return p;}
}c1;
posted @ 2023-06-10 11:17  cjrqwq  阅读(4)  评论(0编辑  收藏  举报  来源