树状数组

\(emm……\),作为一个完全可以被线段树代替的数据结构,其主要优点只有代码短与常数小
然而总有无聊的出题人卡常以及类似我的蒟蒻调不出线段树,所以还是得学一下的

树状数组天然用来维护前缀和,所以支持区间修改,单点查询;单点修改,区间查询
如果非要区间修改,区间修改也不是不行:
首先差分,令 \(d_i=a_i-a_{i-1}\)
\(S_i=\sum_{j=1}^{i}a_j\)
\(=\sum_{j=1}^i d_j(i-j+1)\)
\(=(i+1)\sum_{j=1}^{i}d_j-\sum_{j=1}^{i}jd_j\)
然后开两个树状数组维护 \(d_j\)\(jd_j\) 即可

对于二维树状数组,直接开二维即可
注意内层循环的 \(y\) 不能直接用传进来的参数,应该每次循环都重新定义

关于树状数组上的二分,抄袭 \(cyh\) 代码(附赠改良码风服务

int kth(int k,int x=0){ // 查询第k大
    for(int i=log2(n);i>=0;i--)if(x<n&&k>c[x+(1<<i)])k-=c[x],x+=1<<i;
    return x+1;
}

一个小小小 \(trick\) 是假如更新是单点插入,查询是前缀 \(max\),那么可以直接写成插入时 \(x-=low\),查询时 \(x+=low\) 即可


P6619 [省选联考 2020 A/B 卷] 冰火战士

很明显是对离散化过的位置建出权值树状数组,那么答案一定是 \(sum_1<sum_2\)\(sum_1\ge sum2\) 的临界位置
那么用树状数组上的二分来寻找这两个位置即可
具体实现的时候最好把 \(1\) 类型的位置往后推一个可以方便统一计算

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e6+5;
int q,op[maxn],cla[maxn],x[maxn],y[maxn],lsh[maxn],tot,sum,k[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct BIT{
	int c[maxn];
	void add(int x,int w){for(;x<=tot;x+=x&-x)c[x]+=w;}
	int ask(int x,int res=0){for(;x;x-=x&-x)res+=c[x];return res;}
	int w(int x){return c[x];}
}tr[2];
void ask(){
//	cout<<"kkk "<<sum<<endl;
	int cur=0,suma=0,sumb=sum;
	for(int i=20,w0,w1;i>=0;i--){
		cur+=1<<i,suma+=w0=tr[0].w(cur),sumb-=w1=tr[1].w(cur);
//		cout<<"hhh "<<cur<<" "<<tot<<" "<<suma<<" "<<sumb<<endl;
		if(cur<=tot&&suma<sumb)continue;
		cur-=1<<i,suma-=w0,sumb+=w1;
	}
	int resa=suma,posa=cur;suma=0,sumb=sum,cur=0;
	int resb=min(tr[0].ask(posa+1),sum-tr[1].ask(posa+1));
	if(resa>resb){
		printf("%d %lld\n",lsh[posa],resa*2);
		return ;	
	}
//	cout<<posa<<" "<<resa<<"  "<<min(tr[0].ask(posa),sum-tr[1].ask(posa))<<endl;
//	cout<<"kkk "<<resa<<" "<<resb<<" "<<posa<<endl;
	for(int i=20,w0,w1;i>=0;i--){
		cur+=1<<i,suma+=w0=tr[0].w(cur),sumb-=w1=tr[1].w(cur);
		if(cur<=tot&&(suma<sumb||min(suma,sumb)==resb))continue;
		cur-=1<<i,suma-=w0,sumb+=w1;
	}
	if(!resb)puts("Peace");
	else printf("%d %lld\n",lsh[cur],resb*2);
}
signed main(){
	q=read();
	for(int i=1;i<=q;i++){
		op[i]=read();
		if(op[i]==1)cla[i]=read(),lsh[++tot]=x[i]=read(),y[i]=read();
		else k[i]=read();
	}
	sort(lsh+1,lsh+tot+1);tot=unique(lsh+1,lsh+tot+1)-lsh-1;
	for(int i=1;i<=q;i++)if(x[i])x[i]=lower_bound(lsh+1,lsh+tot+1,x[i])-lsh;
	for(int i=1;i<=q;i++){
		if(op[i]==1){
			if(!cla[i])tr[0].add(x[i],y[i]);
			else tr[1].add(x[i]+1,y[i]),sum+=y[i];
		}
		else{
			if(!cla[k[i]])tr[0].add(x[k[i]],-y[k[i]]);
			else tr[1].add(x[k[i]]+1,-y[k[i]]),sum-=y[k[i]];
		}
		ask();
	}
	return 0;
}

分块套树状数组

相当于是一个二维树状数组,只不过把第一维开在了块上,这样就可以在时空复杂度正确的情况下维护信息了
但是注意此时复杂度的保证是点是离散的,保证一个横坐标对应的点不会很多,这样零散块才可以很方便地暴力
复杂度好像比较神奇,是 \(O(\sqrt n+log^2n)\),因为分块的根号是跟在树状数组的 \(log\) 后面的,因此不会有 \(log\) 乘根号这一项,在许多情境下运行效率大大优于树套树


CF1093E Intersection of Permutations

可以发现这道题的点是一个排列,于是满足了使用条件
注意由于树状数组是前缀和型的,因此询问应当拆分成容斥形式

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
const int maxm=505;
int n,m,re[maxn],_y[maxn],op,x,y,xx,yy,sum[maxm][maxn],num,siz,l[maxn],r[maxn],c[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
void add(int pos,int w,int op){
	for(int i=c[pos];i<=num;i+=i&-i)
		for(int j=w;j<=n;j+=j&-j)
			sum[i][j]+=op;
	return ;
}
void change(int x,int y){
	add(x,_y[x],-1),add(y,_y[y],-1);
	swap(_y[x],_y[y]);
	add(x,_y[x],1),add(y,_y[y],1);
	return ;
}
int ask(int pos,int w){
	if(!pos)return 0;int res=0;
	for(int i=l[c[pos]];i<=pos;i++)res+=_y[i]<=w;
	for(int i=c[pos]-1;i;i-=i&-i)
		for(int j=w;j;j-=j&-j)
			res+=sum[i][j];
	return res;
}
int ask(int y,int yy,int x,int xx){
	return ask(xx,yy)-ask(xx,y-1)-ask(x-1,yy)+ask(x-1,y-1);
}
int main(){
	n=read();m=read();for(int i=1,x;i<=n;i++)x=read(),re[x]=i;
	for(int i=1,x;i<=n;i++)x=read(),_y[i]=re[x];
	siz=sqrt(n),num=(n-1)/siz+1;
	for(int i=1;i<=n;i++)c[i]=(i-1)/siz+1;
	for(int i=1;i<=num;i++)l[i]=r[i-1]+1,r[i]=min(n,siz*i);
	for(int i=1;i<=n;i++)add(i,_y[i],1);
	while(m--){
		op=read();x=read(),y=read();
		if(op==1){
			xx=read(),yy=read();
			printf("%d\n",ask(x,y,xx,yy));
		}
		else change(x,y);
	}
	return 0;
}

树状数组套主席树

可以用于求解区间第 \(k\) 大,如果不需离散化可以在线进行
类似于线段树套平衡树,只不过把线段树替换为更快的树状数组,而平衡树+二分的过程变为了权值线段树上的二分
只需要记录出这一过程中树状数组上所有树根是什么,线段树的二分工作判断时应当统计当前所有树根的信息然后进行判断,时间复杂度为 \(log^2n\),效率较高

posted @ 2022-07-23 21:46  y_cx  阅读(93)  评论(0编辑  收藏  举报