数据结构进阶

数列分块

基础

对于 分块模板 ,思路是最直接的一个,将原来的整个序列分成几块。

  • 对于要处理的区间内被完全包含的整块,直接打下懒标记 tag ,然后进行批量处理。

  • 对于左右两边零散的块,暴力处理。

单次修改时间复杂度: \(\mathcal O (\sqrt N)\)

可食用范围:

  1. 对于要写 区修区查 ,但是不想写 线段树 \(or\) 树状数组 的人十分适用。

  2. 有一些操作比较复杂,可能只能用 分块 做。

CODE:

数列分块入门2-区修区查

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=50005;
int n;
int a[N],sum[N],add[N];
int L[N],R[N],pos[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void build(){
	int len=sqrt(n);
	for(int i=1;i<=len;i++){
		L[i]=(i-1)*len+1;
		R[i]=i*len;
	}
	if(R[len]<n) len++,L[len]=R[len-1]+1,R[len]=n;
	for(int i=1;i<=len;i++){
		for(int j=L[i];j<=R[i];j++)
			sum[i]+=a[j],pos[j]=i;
	}
}
void change(int l,int r,int c){
	int x=pos[l],y=pos[r];
	if(x==y){
		for(int i=l;i<=r;i++)
			a[i]+=c;
		sum[x]+=(r-l+1)*c;
		return;
	}
	for(int i=l;i<=R[x];i++) a[i]+=c,sum[x]+=c;
	for(int i=L[y];i<=r;i++) a[i]+=c,sum[y]+=c;
	for(int i=x+1;i<=y-1;i++) add[i]+=c,sum[i]+=(R[i]-L[i]+1)*c;
}
int ask(int l,int r,int m){
	int x=pos[l],y=pos[r],ans=0;
	if(x==y){
		for(int i=l;i<=r;i++)
			ans=(ans+a[i]+add[x])%m;
		return ans;
	}
	for(int i=l;i<=R[x];i++) ans=(ans+a[i]+add[x])%m;
	for(int i=L[y];i<=r;i++) ans=(ans+a[i]+add[y])%m;
	for(int i=x+1;i<=y-1;i++) ans=(ans+sum[i])%m;
	return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	build();
	for(int i=1;i<=n;i++){
		int opt,l,r,c;cin>>opt>>l>>r>>c;
		if(opt==0) change(l,r,c);
		else cout<<ask(l,r,c+1)<<'\n';
	}
	return 0;
}               

数列分块应用

数列分块入门8-区间众数

这个也是不想说些什么了,仅作示例,以供展示 数列分块 的作用。

首先我们可以确定我们的答案要不是在整块中的书,要不是在整块外。

  • 整块: 我们可以先预处理出整块 [l,r] 的众数,然后现在考虑两边的数。

  • 散块: 我们直接枚举两边的数,然后以值域创建记录下表的vector<int>,直接二分得到其数量。

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=100005,M=2020;
int n,len;
int a[N],f[M][M][3],back[N];
int L[M],R[M],pos[N];
unordered_map<int,int> mp;
vector<int> v[N];
int vis[M][N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void build(){
	len=min((int)sqrt(n),80);
	int i;
	for(i=1;;i++){
		L[i]=R[i-1]+1;
		R[i]=min(L[i]+len,n);
		if(R[i]==n)break;
	}
	len=i;
	for(int i=1;i<=len;i++){
		for(int j=L[i];j<=R[i];j++)
			pos[j]=i;
	}
	for(int i=1;i<=len;i++){
		int ans=0,maxn=-1;
		for(int j=i;j<=len;j++){
			for(int k=L[j];k<=R[j];k++){
				int id=a[k];
				vis[i][id]++;
				if(vis[i][id]>maxn) maxn=vis[i][id],ans=id;
				if(vis[i][id]==maxn&&back[a[k]]<back[ans]) maxn=vis[i][id],ans=id;
			}
			f[i][j][1]=ans,f[i][j][2]=maxn;
		}
	}
}
int get(int l,int r,int x){
	int ans1,ans2,ll=0,rr=v[x].size()-1;
	while(ll<rr){
		int mid=(ll+rr)>>1;
		if(v[x][mid]>=l) rr=mid;
		else ll=mid+1;
	}
	ans1=ll;
	ll=0,rr=v[x].size()-1;
	while(ll<rr){
		int mid=(ll+rr+1)>>1;
		if(v[x][mid]<=r) ll=mid;
		else rr=mid-1;
	}
	ans2=ll;
	return ans2-ans1+1;
}
inline void work(int &ans,int &mx,int i,int l,int r){
	int an=get(l,r,a[i]);
	if(an>mx) mx=an,ans=a[i];
	else if(an==mx and back[a[i]]<back[ans]) ans=a[i];	
}
int ask(int l,int r){
	int x=pos[l],y=pos[r];
	if(x==y){
		int ans=0,maxn=-1;
		for(int i=l;i<=r;i++) work(ans,maxn,i,l,r);
		return ans;
	}
	int ans=0,maxn=-1;
	if(x+1<=y-1) ans=f[x+1][y-1][1],maxn=f[x+1][y-1][2];
	for(int i=l;i<=R[x];i++) work(ans,maxn,i,l,r);
	for(int i=L[y];i<=r;i++) work(ans,maxn,i,l,r);
	return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int cnt=0;
	for(int i=1;i<=n;i++){
		if(mp[a[i]]==0)
		    mp[a[i]]=++cnt,back[cnt]=a[i];
		a[i]=mp[a[i]];
		v[a[i]].push_back(i);
	}
	build();
	back[0]=1e18;
	for(int i=1;i<=n;i++){
		int l,r;cin>>l>>r;
		cout<<back[ask(l,r)]<<'\n';
	}
	return 0;
}

树状数组

我认为 树状数组 绝对是最简单实用的数据结构。

基本操作

如下图:

对于 add 操作: (对于 0b101)

我们需要对于这些数加: 0b1010b1100b10000b10000

我么发现如果现在处理了 x ,则下一次为 x' = x + lowbit(x)

对于 ask 操作:(对于 0b1111

我们需要将答案加上: 0b11110b11100b11000b1000

我么发现如果现在处理了 x ,则下一次为 x' = x - lowbit(x)

CODE:

struct tree{
	int c[N];
	void add(int x,int a){
		for(int i=x;i<=n;i+=(i&-i)) c[i]+=a;
	}
	int ask(int x){
		int ans=0;
		for(int i=x;i;i-=(i&-i)) ans+=c[i];
		return ans;
	}
}BIT;

优劣之处

优点: 时间复杂度 : \(\mathcal O (Q log N)\) ,空间复杂度 \(\mathcal O (N)\),常数十分小,代码短。

劣势: 对于维护 max,min , 则无法完成。

注意事项

树状数组 只能维护下标大于0,否则会死循环。

线段树

普通线段树

线段树可以说是 数据结构进阶 中的相对重要的部分了。

原理其实和 树状数组 比较相近,如下图:

当我们要处理区间 [3,8],其实是将其分为:[3,3],[4,5],[6,7],[8,8],然后整块进行批量处理。

CODE:

单修区查

class SMT{
    private:
        int cnt=0;
        struct SegmentTree{
            int lc,rc;
            int sum;
            #define ls T[p].lc
            #define rs T[p].rc
            SegmentTree(){
                lc=rc=sum=0;
            }
        }T[N<<2];
        void push_up(int p){
            T[p].sum=T[ls].sum+T[rs].sum;
        }
    public:
        int root=0;
        void change(int &p,int l,int r,int x,int y){
            if(!p) p=++cnt;
            if(l==r){
                T[p].sum+=y;
                return;
            }
            int mid=l+r>>1;
            if(x<=mid) change(ls,l,mid,x,y);
            else change(rs,mid+1,r,x,y);
            push_up(p);
        }
        int ask(int p,int l,int r,int x,int y){
            if(!p) return 0;
            if(x<=l && r<=y) return T[p].sum;
            int mid=l+r>>1,ans=0;
            if(x<=mid) ans+=ask(ls,l,mid,x,y);
            if(y>mid) ans+=ask(rs,mid+1,r,x,y);
            return ans;
        }
};

区修区查

/*由于普通线段树需要build,这里直接写的动态开点*/
const int N=1e6+5;
namespace SMT{
    int root;
    int cnt;
    struct SigmentTree{
        int l,r;
        int lc,rc;
        int sum,tag;
        #define ls T[p].lc
        #define rs T[p].rc
    }T[N<<2];
    void push_down(int p){
        if(T[p].tag){
            int mid=T[p].l+T[p].r>>1;
            if(ls==0) {
                ls=++cnt;
                T[ls]={T[p].l,mid,0,0,0,0};
            }
            if(rs==0) {
                rs=++cnt;
                T[rs]={mid+1,T[p].r,0,0,0,0};
            }
            T[ls].sum+=(T[ls].r-T[ls].l+1)*T[p].tag;
            T[rs].sum+=(T[rs].r-T[rs].l+1)*T[p].tag;
            T[ls].tag+=T[p].tag;
            T[rs].tag+=T[p].tag;
            T[p].tag=0;
        }
    }
    void push_up(int p){
        T[p].sum=T[ls].sum+T[rs].sum;
    }
    void change(int &p,int l,int r,int x,int y,int k){
        if(!p) p=++cnt,T[p]={l,r,0,0,0,0};
        if(x<=l && r<=y){
            T[p].sum+=(r-l+1)*k;
            T[p].tag+=k;
            return;
        }
        push_down(p);
        int mid=l+r>>1;
        if(x<=mid) change(ls,l,mid,x,y,k);
        if(y>mid) change(rs,mid+1,r,x,y,k);
        push_up(p);
    }
    int ask(int p,int l,int r,int x,int y){
        if(!p) return 0;
        if(x<=l && r<=y) return T[p].sum;
        push_down(p);
        int mid=l+r>>1,ans=0;
        if(x<=mid) ans=(ans+ask(ls,l,mid,x,y));
        if(y>mid) ans=(ans+ask(rs,mid+1,r,x,y));
        return ans;
    }
}
using namespace SMT;

权值线段树

及将权值当作下标存贮,可以处理 前驱,后缀,一个数字的个数 等问题。

CODE:

/*
这就不放了,反正和前面的一样,只是用法不同。
*/

标记永久化

这里主要是处理下面的 主席树(可持久化线段树) ,因为在 主席树 中下传标记容易出错,因而我们可以想出一种办法不用标记下传。

及把标记留在原地,让后面查询时加上。

CODE:

长序列区修区查

#include<bits/stdc++.h>
#define int long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+5;
int n,m,root;
int lc[N],rc[N],sum[N],tag[N],cnt;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline void change(int &p,int l,int r,int x,int y,int k){
	if(!p) p=++cnt;
	if(x<=l&&r<=y){
		sum[p]+=k*(r-l+1);
		tag[p]+=k;
	    return;
	}
	sum[p]+=k*(min(r,y)-max(x,l)+1);
	int mid=l+r>>1;
	if(x<=mid) change(lc[p],l,mid,x,y,k);
	if(y>mid) change(rc[p],mid+1,r,x,y,k);
}
inline int ask(int &p,int l,int r,int x,int y,int val){
	if(!p) return val*(min(r,y)-max(x,l)+1);
	if(x<=l&&r<=y) return sum[p]+val*(r-l+1);
	int mid=l+r>>1,ans=0;
	if(x<=mid) ans+=ask(lc[p],l,mid,x,y,val+tag[p]);
	if(y>mid) ans+=ask(rc[p],mid+1,r,x,y,val+tag[p]);
	return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
	while(m--){
		int opt,l,r;cin>>opt>>l>>r;
		if(opt==1){
			int k;cin>>k;
			change(root,1,n,l,r,k);
		}else cout<<ask(root,1,n,l,r,0)<<'\n';
	}
	return 0;
}

线段树合并与分裂

合并

首先 线段树合并 分两种,一种为会覆盖原树,另一种反之。

第一种

void Merge(int &a,int b){
	if(!a||!b) {a=a+b;return;}
	sum[a]=sum[a]+sum[b];
	Merge(lc[a],lc[b]);
	Merge(rc[a],rc[b]);
}

第二种

int Merge(int &a,int b){
	if(a==0||b==0) {return a+b;}
	int p=++cnt;
	sum[p]=sum[a]+sum[b];
	lc[p]=Merge(lc[a],lc[b]);
	rc[p]=Merge(rc[a],rc[b]);
	return p;
}

但这其实是骗你的,第二种也会覆盖,如果我们changea==0 || b==0 , 的节点,其仍然会将原来的点更改,这种不被覆盖是相对的,覆盖指的是在下一次merge的时候不被更改。

分裂

分裂可以参考 FHQ Treap 的分裂方法。

CODE:

void split(int &a,int &b,int l,int r,int x,int y){
	if(!a) return;
	if(x<=l&&r<=y){
		b=a,a=0;
		return;
	}
	if(!b) b=++cnt;
	int mid=l+r>>1;
	if(x<=mid) split(lc[a],lc[b],l,mid,x,y);
	if(y>mid) split(rc[a],rc[b],mid+1,r,x,y);
	push_up(a),push_up(b);
}
/*转了一圈发现自己好像除了模板题其他没有写过一道*/

可持久化线段树

原理如下图:

当我们进行 change 操作时,除了对于访问的节点新建节点,其他的直接连上以前的节点,因而空间复杂度为:

\[\mathcal O(4N+Q \log N) \]

因而这样可以记录下每一个时间辍的线段树,再结合前缀和思想,可以处理对于以前版本的访问与更改。

CODE:
可持久化数组

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+5;
int n,m;
int tot,a[N],root[N];
int cnt,lc[N*32],rc[N*32],sum[N*32];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline void build(int &p,int l,int r){
	p=++cnt;
	if(l==r){sum[p]=a[l];return;}
	int mid=l+r>>1;
	build(lc[p],l,mid);
	build(rc[p],mid+1,r);
}
inline void change(int &a,int b,int l,int r,int x,int k){
	a=++cnt;
	if(l==r){sum[a]=k;return;}
	lc[a]=lc[b],rc[a]=rc[b];
	int mid=l+r>>1;
	if(x<=mid) change(lc[a],lc[b],l,mid,x,k);
	if(x>mid) change(rc[a],rc[b],mid+1,r,x,k);
}
inline int ask(int &p,int l,int r,int x){
	if(!p) return 0;
	if(l==r) return sum[p];
	int mid=l+r>>1;
	if(x<=mid) return ask(lc[p],l,mid,x);
	if(x>mid) return ask(rc[p],mid+1,r,x);
}
inline int read(){//快读
	int ans=0,j=1;char c=getchar();
	while(c>'9' or c<'0'){if(c=='-')j=-1;c=getchar();}
	while(c>='0' and c<='9'){ans=ans*10+c-'0';c=getchar();}
	return ans*j;
}
inline void write(int x){//快写
	if(x<0){putchar('-');x=-x;}
	if(!x)return;write(x/10);putchar(x%10+'0');
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
    n=read(),m=read();
    for(int i=1;i<=n;i++) a[i]=read();
    build(root[0],1,n);
    for(int i=1;i<=m;i++){
    	int a=read(),opt=read(),b=read();
    	if(opt==1){
    		int k=read();
    		change(root[i],root[a],1,n,b,k);
		}else {
			root[i]=root[a];
			write(ask(root[a],1,n,b));
            puts("");
		}
	}
	return 0;
}

而对于区修区查,则需要使用前面提到的标记永久化。

CODE:

时光旅行者的数组维护

#include<bits/stdc++.h>
#define ll long long
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e6+6;
int n,m,cnt;
int a[N],root[N];
struct node{
	int lc,rc;
	ll sum,tag;
}t[N*60];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
inline int read(){//快读
	int ans=0,j=1;char c=getchar();
	while(c>'9' or c<'0'){if(c=='-')j=-1;c=getchar();}
	while(c>='0' and c<='9'){ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();}
	return ans*j;
}
void build(int &p,int l,int r){
	p=++cnt;
	if(l==r){
		t[p].sum=a[l];
		return;
	}
	int mid=l+r>>1;
	build(t[p].lc,l,mid);
	build(t[p].rc,mid+1,r);
	t[p].sum=t[t[p].lc].sum+t[t[p].rc].sum;
}
void change(int &a,int b,int l,int r,int x,int y,int k){
	a=++cnt,t[a]=t[b];
	if(x<=l && r<=y){
		t[a].sum+=(r-l+1)*k;
		t[a].tag+=k;
		return;
	}
	t[a].sum+=(min(y,r)-max(x,l)+1)*k;
	int mid=l+r>>1;
	if(x<=mid) change(t[a].lc,t[b].lc,l,mid,x,y,k);
	if(y>mid) change(t[a].rc,t[b].rc,mid+1,r,x,y,k);
}
ll ask(int p,int l,int r,int x,int y,ll val){
	if(!p) return (min(y,r)-max(x,l)+1)*val;
	if(x<=l && r<=y) return t[p].sum+(r-l+1)*val;
	int mid=l+r>>1;
	ll ans=0;
	if(x<=mid) ans+=ask(t[p].lc,l,mid,x,y,val+t[p].tag);
	if(y>mid) ans+=ask(t[p].rc,mid+1,r,x,y,val+t[p].tag);
	return ans;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	build(root[0],1,n);
	for(int i=1;i<=m;i++){
		int v=read(),opt=read(),l=read(),r=read(),k;
		if(opt==1){
			k=read();
			change(root[i],root[v],1,n,l,r,k);
		}else{
			root[i]=root[v];
			ll ans=ask(root[v],1,n,l,r,0);
			printf("%lld\n",ans);
		}
	}
	return 0;
}

线段树分治

线段树分治 并不是在线段树上分支,而是以操作的时间戳建树,能够处理操作需要反悔的情况。

可以直接上例题:

动态图连通性

我们发现这道题其实只有在一段时间内才会加上某一条边,于是可以使用 线段树分治 ,以操作的时间戳建树,然后遍历整棵线段树。

所以我们仍需要知道如何反悔,因为在 如下图 中的处理时候,需要删去一些边。

这时候可能会出现一些疑问,既然这里也要删去一些边,为和不直接暴力做呢?

这是因为如果我们把插入了边当作放入一个栈中,现在我们删去的边一定在栈顶。

所以我们可以直接在记录下加入一条边时,两个(一个)点的fa[]值,可以发现一定不会出现影响。

而如果在暴力中这样做,会发现有可能现在删除的边更新的fa[]已经影响到了其他边,故直接删除会有错。

CODE:

#include<bits/stdc++.h>
#define lc p<<1
#define rc p<<1|1
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=5e3+5,M=5e5+5;
int n,m,top;
struct node{
	int op,x,y;
}opt[M],s[M];
int fa[M],h[M];
int lst[N][N];
vector<node> v[M<<2];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void change(int p,int l,int r,int x,int y,int a,int b){
	if(x<=l && r<=y){
		v[p].push_back({0,a,b});
		return;
	}
	int mid=l+r>>1;
	if(x<=mid) change(lc,l,mid,x,y,a,b);
	if(y>mid) change(rc,mid+1,r,x,y,a,b);
}
void init(){
	for(int i=1;i<=m;i++){
		int op=opt[i].op,x=opt[i].x,y=opt[i].y;
		if(op==1) lst[x][y]=i;
		if(op==2) {
			change(1,1,m,lst[x][y],i-1,x,y);
			lst[x][y]=0;
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(lst[i][j]) change(1,1,m,lst[i][j],m,i,j);
		}
	}
}
int find(int x){
	if(x==fa[x]) return x;
	return find(fa[x]);
}
void Merge(int x,int y){//把y放在x
	int rx=find(x),ry=find(y);
	if(rx==ry) return;
	if(h[rx]<h[ry]) swap(rx,ry);
	s[++top]={h[rx],rx,ry};
	fa[ry]=rx,h[rx]+=(h[rx]==h[ry]);
}
void dfs(int p,int l,int r){
	int now=top;
	for(int i=0;i<v[p].size();i++){
		Merge(v[p][i].x,v[p][i].y);
	}
	int mid=l+r>>1;
	if(l==r){
		if(opt[l].op==3){
			if(find(opt[l].x)==find(opt[l].y)) puts("Y");
			else puts("N");
		}
	}else{
		dfs(lc,l,mid);
		dfs(rc,mid+1,r);
	}
	while(now<top){
		fa[s[top].y]=s[top].y;
		h[s[top].x]=s[top].op;
		top--;
	}
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		int op,x,y;scanf("%d%d%d",&op,&x,&y);
		if(x>y) swap(x,y);
		opt[i]={op+1,x,y};
	}
	for(int i=1;i<=n;i++) fa[i]=i;
	init();
	dfs(1,1,m);
	return 0;
}

CDQ分治

普通 CDQ分治

CDQ分治 主要用于求解偏序问题,如这道题目 P3810 【模板】三维偏序(陌上花开)

CDQ分治 是分区间解决这个问题,假设我们现在正在处理 [l,r] 的信息,我们可以将其分为:[l,mid][mid+1,r] 解决,左右两边可以递归解决,现在考虑 i \(\in\) [l,mid]j \(\in\) [mid+1,r]。的信息。

对于三维偏序,我们如下处理:

  • 第一层: 直接排序,则如果ij,满足i < j 则满足。

  • 第二层: 在 CDQ分治 内部左边右边分别按第二层排序,然后在左右区间设定 双指针 ,即可满足。 这时候第一层依然满足,因为我们仅对 左右区间打乱,左右 i < j

  • 第三层: 在 双指针 实现同时将元素打入 树状数组 中,查询即可。

于是就有了代码:

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=2e5+5;
int n,m,k;
int f[N];
struct node{//元素信息
	int a,b,c;
	int cnt,ans;
}e[N],a[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
struct tree{
	int c[N];
	void add(int x,int a){
		for(int i=x;i<N;i+=(i&-i)) c[i]+=a;
	}
	int ask(int x){
		int ans=0;
		for(int i=x;i;i-=(i&-i)) ans+=c[i];
		return ans;
	}
}BIT;
bool operator != (node a,node b){
	return a.a!=b.a||a.b!=b.b||a.c!=b.c;
}
bool cmpA(node a,node b){
	if(a.a!=b.a) return a.a<b.a;
	if(a.b!=b.b) return a.b<b.b;
	return a.c<b.c;
}
bool cmpB(node a,node b){
	if(a.b!=b.b) return a.b<b.b;
	return a.c<b.c;
}
void CDQ(int l,int r){
	if(l==r) return;
	int mid=l+r>>1;
	CDQ(l,mid),CDQ(mid+1,r);
	sort(a+l,a+mid+1,cmpB);
	sort(a+mid+1,a+r+1,cmpB);
	int i=l,j=mid+1;
	while(j<=r&&i<=mid){
		if(a[i].b<=a[j].b) BIT.add(a[i].c,a[i].cnt),i++;
		else a[j].ans+=BIT.ask(a[j].c),j++;
	}
	while(i<=mid) BIT.add(a[i].c,a[i].cnt),i++;
	while(j<=r) a[j].ans+=BIT.ask(a[j].c),j++;
	for(int ii=l;ii<=mid;ii++) BIT.add(a[ii].c,-a[ii].cnt);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>e[i].a>>e[i].b>>e[i].c;
	sort(e+1,e+n+1,cmpA);
	int t=0;
	for(int i=1;i<=n;i++){
		t++;
		if(e[i]!=e[i+1]) a[++m]=e[i],a[m].cnt=t,t=0;
	}
	CDQ(1,m);
	for(int i=1;i<=m;i++) f[a[i].ans+a[i].cnt-1]+=a[i].cnt;
	for(int i=0;i<n;i++) cout<<f[i]<<'\n';
	return 0;
}

四维CDQ分治

主要是多的一维:

直接打上标记,然后进行正常CDQ:

  • 第一层: 直接排序,则如果ij,满足i < j 则满足。

  • 第二层: 打上标记。

  • 第三层: 在 CDQ分治 内部左边右边分别按第二层排序,然后在左右区间设定 双指针 ,即可满足。 这时候第一层依然满足,因为我们仅对 左右区间打乱,左右 i < j

  • 第四层: 在 双指针 实现同时将元素打入 树状数组 中,查询即可。

CODE:

[CH弱省胡策R2] TATT

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e5+5;
int n;//点的数量
int k;//去重后点的数量
int m;//离散化后d的数量
int h[N];//Hash
struct node{
	int a,b,c,d;//四个位置
	int w;//贡献
	int ans;//答案
	int falg;//标记左边,右边
	int id;//原位置
	bool operator ==(const node &p) const{
		return a==p.a&&b==p.b&&c==p.c&&d==p.d;
	}
}u[N],tmp[N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
bool cmpA(node a,node b){
	if(a.a!=b.a) return a.a<b.a;
	if(a.b!=b.b) return a.b<b.b;
	if(a.c!=b.c) return a.c<b.c;
	return a.d<b.d;
}
bool cmpB(node a,node b){
	if(a.b!=b.b) return a.b<b.b;
	if(a.c!=b.c) return a.c<b.c;
	if(a.d!=b.d) return a.d<b.d;
	return a.a<b.a;
}
bool cmpC(node a,node b){
	if(a.c!=b.c) return a.c<b.c;
	if(a.d!=b.d) return a.d<b.d;
	if(a.a!=b.a) return a.a<b.a;
	return a.b<b.b;
}
struct tree{
	int c[N];
	void add(int x,int a){
		for(int i=x;i<=m;i+=(i&-i)) c[i]=max(c[i],a);
	}
	int ask(int x){
		int ans=0;
		for(int i=x;i;i-=(i&-i)) ans=max(ans,c[i]);
		return ans;
	}
	void clear(int x){
		for(int i=x;i<=m;i+=(i&-i)) c[i]=0;
	}
}BIT;
void CDQ2(int l,int r){
	if(l==r) return;
	int mid=l+r>>1;
	CDQ2(l,mid),CDQ2(mid+1,r);
	int i=l,j=mid+1,tot=l;
	while(j<=r&&i<=mid){
		if(u[i].c<=u[j].c){
			if(u[i].falg==1) BIT.add(u[i].d,u[i].ans);
			tmp[tot++]=u[i++];
		}
		else{
			if(u[j].falg==0) u[j].ans=max(u[j].ans,BIT.ask(u[j].d)+u[j].w);
			tmp[tot++]=u[j++];
		}
	}
	while(i<=mid) {
		if(u[i].falg==1) BIT.add(u[i].d,u[i].ans);
		tmp[tot++]=u[i++];
	}
	while(j<=r) {
		if(u[j].falg==0) u[j].ans=max(u[j].ans,BIT.ask(u[j].d)+u[j].w);
		tmp[tot++]=u[j++];
	}
	for(i=l;i<=mid;i++) if(u[i].falg) BIT.clear(u[i].d);
	for(i=l;i<=r;i++) u[i]=tmp[i];
}
void CDQ1(int l,int r){
	if(l==r) return;
	int mid=l+r>>1;
	CDQ1(l,mid);
	for(int i=l;i<=mid;i++) u[i].falg=1;
	for(int i=mid+1;i<=r;i++) u[i].falg=0;
	
	sort(u+l,u+r+1,cmpB);
	CDQ2(l,r);
	
	for(int i=l;i<=r;i++) tmp[u[i].id]=u[i];
	for(int i=l;i<=r;i++) u[i]=tmp[i];
	
	CDQ1(mid+1,r);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>u[i].a>>u[i].b>>u[i].c>>u[i].d;
		h[i]=u[i].d,u[i].w=1;
	}
	sort(h+1,h+n+1);
	sort(u+1,u+n+1,cmpA);
	m=unique(h+1,h+n+1)-h-1;//Hash
	k=1;
	for(int i=2;i<=n;i++){
		if(u[i]==u[k]) u[k].w++;
		else u[++k]=u[i];
	}
	for(int i=1;i<=k;i++){
		u[i].id=i;
		u[i].ans=u[i].w;
		u[i].d=lower_bound(h+1,h+m+1,u[i].d)-h;
	}
	CDQ1(1,k);
	int ans=0;
	for(int i=1;i<=k;i++) ans=max(ans,u[i].ans);
	cout<<ans;
	return 0;
}

N维CDQ分治

来观摩以下 5维CDQ分治

//在想什么呢,则么可能写的出来

整体二分

题目:

整体二分模板

点进去你应该看到的是 P3834 【模板】可持久化线段树 2 ,请仔细看标签。

这个模板题其实就是解决多次二分而生的,我在写这里时也不知道为什么他会在这里。

我们可以将要查询的区间先离线下来,然后统一递归处理。

这里我们递归的参数为 void solve(int l,int r,int al,int ar); 为 答案左右区间 和 操作左右区间。

然后处理 [al,ar] 的操作,根据其比 mid 大的数的个数分配递归的位置,这里修改操作一定在前面。

CODE:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=2e5+5;
int n,m,tot;
int ans[N];
struct node{
	int op,x,y,k;
	int id;
}p[N<<1],ll[N<<1],rr[N<<1];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
struct tree{
	int c[N];
	void add(int x,int a){
		for(int i=x;i<=n;i+=(i&-i)) c[i]+=a;
	}
	int ask(int x){
		int ans=0;
		for(int i=x;i;i-=(i&-i)) ans+=c[i];
		return ans;
	}
}BIT;
void solve(int l,int r,int al,int ar){
	if(l>r || al>ar) return;
	if(l==r){
		for(int i=al;i<=ar;i++)
		    if(p[i].op) ans[p[i].id]=l;
		return;
	}
	int mid=l+r>>1;//假设所有的第k小皆为mid
	int nl=0,nr=0;
	for(int i=al;i<=ar;i++){
		if(p[i].op==0){//修改
			if(p[i].y<=mid){//x<=mid
				BIT.add(p[i].x,1);
			    ll[++nl]=p[i];
			}else rr[++nr]=p[i];
		}else{//查询
			int s=BIT.ask(p[i].y)-BIT.ask(p[i].x-1);
			if(s>=p[i].k) ll[++nl]=p[i];
			//在[l,r]的区间内小于mid的个数大于等于k,相当于mid>=ans
			else p[i].k-=s,rr[++nr]=p[i];
			//在[l,r]的区间内小于mid的个数小于k,相当于mid<ans
		}
	}
	for(int i=1;i<=nl;i++) p[al+i-1]=ll[i];
	for(int i=1;i<=nr;i++) p[al+nl+i-1]=rr[i];
	for(int i=al;i<=ar;i++)
		if(p[i].id==0&&p[i].y<=mid) BIT.add(p[i].x,-1);
	solve(l,mid,al,al+nl-1);
	solve(mid+1,r,al+nl,ar);
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int x;cin>>x;
		p[++tot]={0,i,x,0,0};
	}
	for(int i=1;i<=m;i++){
		int x,y,k;cin>>x>>y>>k;
		p[++tot]={1,x,y,k,i};
	}
	solve(1,1e9,1,tot);
	for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
	return 0;
}

平衡树

FHQ-Treap学习笔记 - 洛谷专栏

浅谈 Splay 伸展树 - 洛谷专栏

多么好的总结啊!!!

对于平衡树,我最熟的仍是 FHQ-Treap

CODE:

所有代码,均针对:P3369 【模板】普通平衡树

Splay:

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e5+5;
int n;//n
int root;//树之根
int tot;//开点编号
struct node{
	int s[2];//左右儿子
	int fa;//父亲
	int val;//节点数字
	int cnt;//重复个数
	int siz;//子树大小
	#define ls T[p].s[0]
	#define rs T[p].s[1]
}T[N<<1];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void push_up(int p){
	T[p].siz=T[ls].siz+T[rs].siz+T[p].cnt;
}
inline void rotate(int x){
	int y=T[x].fa,z=T[y].fa;
	int k1=(T[y].s[1]==x);//k=0:x是y的左儿子 k=1:x是y的右儿子
	int k2=(T[z].s[1]==y);//k=0:y是z的左儿子 k=1:y是z的右儿子
	T[z].s[k2]=x,T[x].fa=z;
	T[y].s[k1]=T[x].s[k1^1];
	T[T[x].s[k1^1]].fa=y;
	T[x].s[k1^1]=y,T[y].fa=x;
	push_up(y),push_up(x);
}
inline void Splay(int x,int k){//原点;目标点
	while(T[x].fa!=k){
		int y=T[x].fa,z=T[y].fa;
		if(z!=k) //爸爸的爸爸不是k(爷爷)
			( (T[z].s[0]==y) ^ (T[y].s[0]==x) )?rotate(x):rotate(y);
		rotate(x);
	}
	if(!k) root=x;
}
inline void find(int x){//找到x点;并将其旋转至root
	int p=root;
	while(T[p].s[(x>T[p].val)] && x!=T[p].val)
		p=T[p].s[(x>T[p].val)];
	Splay(p,0);
}
int get_pre(int x){
	find(x);
	int p=root;
	if(T[p].val<x) return p;
	p=ls;
	while(rs) p=rs;
	Splay(p,0);
	return p;
}
int get_suc(int x){
	find(x);
	int p=root;
	if(T[p].val>x) return p;
	p=rs;
	while(ls) p=ls;
	Splay(p,0);
	return p;
}
void ins(int x){
	int p=root,fa=0;
	while(p&&T[p].val!=x)
		fa=p,p=T[p].s[(T[p].val<x)];
	if(p) T[p].cnt++;
	else{
		p=++tot;
		T[fa].s[(T[fa].val<x)]=p;
		T[p]={{0,0},fa,x,1,1};
	}
	Splay(p,0);
}
void del(int x){
	int pre=get_pre(x);
	int suc=get_suc(x);
	Splay(pre,0);
	Splay(suc,pre);
	int p=T[suc].s[0];
	if(T[p].cnt>1){
		T[p].cnt--;
		Splay(p,0);
	}else{
		T[suc].s[0]=0;
		Splay(suc,0);
	}
}
int get_rank(int x){
	ins(x);
	int ans=T[T[root].s[0]].siz;
	del(x);
	return ans;
}
int get_val(int k){
	int p=root;
	while(true){
		if(T[ls].siz+T[p].cnt<k){
			k-=(T[ls].siz+T[p].cnt);
			p=rs;
		}else{
			if(T[ls].siz>=k) {
			    p=ls;
		    }else break;
		}
	}
	Splay(p,0);
	return T[p].val;
}
inline int read(){
	int s=0,f=1; char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=(s<<3)+(s<<1)+(ch^48);ch=getchar();}
	return s*f;
}
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
int main() {
	n=read();
	ins(2147483647);
	ins(-2147483648);
	while(n--) {
		int opt=read(),x=read();
		if(opt==1) ins(x);
		if(opt==2) del(x);
		if(opt==3) printf("%d\n",get_rank(x));
		if(opt==4) printf("%d\n",get_val(x+1));
		if(opt==5) printf("%d\n",T[get_pre(x)].val);
		if(opt==6) printf("%d\n",T[get_suc(x)].val);
	}
    return 0;
}

Head Treap

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,tot,root;
struct node{
	int s[2];
	int val,siz;//点的编号;子树中cnt之和
	int cnt,rnd;//当前节点的重叠数;随即优先级
	#define ls T[p].s[0]
	#define rs T[p].s[1]
}T[N<<1];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void push_up(int p){
	T[p].siz=T[ls].siz+T[rs].siz+T[p].cnt;
}
int new_node(int x){
	T[++tot]={{0,0},x,1,1,rand()};
	return tot;
}
void rotate(int &p,int d){//左旋为0,右旋为1
	int k=T[p].s[d^1];
	T[p].s[d^1]=T[k].s[d];
	T[k].s[d]=p;
	push_up(p),push_up(k);
	p=k;
}
void ins(int &p,int x){
	if(!p){
		p=new_node(x);
		return;
	}
	if(T[p].val==x){
		T[p].cnt++;
		T[p].siz++;
		return;
	}
	int d=x>T[p].val;
	ins(T[p].s[d],x);
	if(T[p].rnd<T[T[p].s[d]].rnd) rotate(p,d^1);
	push_up(p);
}
void del(int &p,int x){
	if(!p) return;
	if(x<T[p].val) del(ls,x);
	else if(x>T[p].val) del(rs,x);
	else{
		if(!ls && !rs){
			T[p].cnt--,T[p].siz--;
			if(T[p].cnt==0) p=0;
		}else if(ls && !rs){
			rotate(p,1);
			del(rs,x);
		}else if(!ls && rs){
            rotate(p,0);
			del(ls,x);
		}else{
			bool d=(T[ls].rnd>T[rs].rnd);
			rotate(p,d);
			del(T[p].s[d],x);
		}
	}
	push_up(p);
}
int ask(int p,int x){
	if(!p)return 0;
	if(T[p].val==x)return T[ls].siz;
	if(T[p].val>x)return ask(ls,x);
	return ask(rs,x)+T[ls].siz+T[p].cnt;
}
int kth(int p,int x){
	if(!p) return 0;
	int ans=T[ls].siz;
	if(ans>=x) return kth(ls,x);
	else if(ans+T[p].cnt>=x) return T[p].val;
	else return kth(rs,x-ans-T[p].cnt);
}
inline int read(){
	int s=0,f=1; char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=(s<<3)+(s<<1)+(ch^48);ch=getchar();}
	return s*f;
}
int main() {
	srand(time(0));
	n=read();
	while(n--) {
		int opt=read(),x=read();
		if(opt==1) ins(root,x);
		if(opt==2) del(root,x);
		if(opt==3) printf("%d\n",ask(root,x)+1);
		if(opt==4) printf("%d\n",kth(root,x));
		if(opt==5) printf("%d\n",kth(root,ask(root,x)));
		if(opt==6) printf("%d\n",kth(root,ask(root,x+1)+1));
	}
    return 0;
}

FHQ-Treap

#include<bits/stdc++.h>
using namespace std;
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
const int N=1e5+5;
int n;
int root,cnt;
struct node{
	int lc,rc;
	int val,siz,rnd;
	#define ls T[p].lc
	#define rs T[p].rc
}T[20*N];
/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
void print(int x){
	if(!x) return;
	print(T[x].lc);
	cout<<T[x].val<<" ";
	print(T[x].rc);
}
void printt(int x){
	cout<<"Case: "<<x<<":\n";
	print(x);
	cout<<"\n\n";
}
void push_up(int p){
	T[p].siz=T[ls].siz+T[rs].siz+1;
}
int new_node(int x){
	T[++cnt]={0,0,x,1,rand()};
	return cnt;
}
void spilt_val(int p,int k,int &x,int &y){//按值分裂
	if(!p) {x=y=0;return;}
	if(T[p].val<=k) x=p,spilt_val(rs,k,T[x].rc,y);
	else y=p,spilt_val(ls,k,x,T[y].lc);
	push_up(p);
}
void spilt_siz(int p,int k,int &x,int &y){//按大小分裂
	if(!p) {x=y=0;return;}
	if(T[p].siz<=k) x=p,spilt_siz(ls,k,T[x].rc,y);
	else y=p,spilt_siz(rs,k,x,T[y].lc);
	push_up(p);
}
int Merge(int x,int y){//返回值为新树的根
	if(!x||!y) return max(x,y);
	if(T[x].rnd<T[y].rnd){
		T[x].rc=Merge(T[x].rc,y);
        push_up(x);
		return x;
	}else{
		T[y].lc=Merge(x,T[y].lc);
		push_up(y);
		return y;
	}
}
void ins(int a){//加入元素a
	int x=0,y=0;//左,右
	spilt_val(root,a,x,y);
//	printt(x),printt(y);
	root=Merge(Merge(x,new_node(a)),y);
}
void del(int a){
	int x,y,z;
	spilt_val(root,a,x,z);
	spilt_val(x,a-1,x,y);
	y=Merge(T[y].lc,T[y].rc);
	root=Merge(Merge(x,y),z);
}
int ask(int a){
	int x,y;
	spilt_val(root,a-1,x,y);
	int ans=T[x].siz+1;
	root=Merge(x,y);
	return ans-1;
}
int kth(int p,int x){
	if(!p) return 0;
	int ans=T[ls].siz;
	if(ans>=x) return kth(ls,x);
	else if(ans+1>=x) return T[p].val;
	else return kth(rs,x-ans-1);
}
inline int read(){
	int s=0,f=1; char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=(s<<3)+(s<<1)+(ch^48);ch=getchar();}
	return s*f;
}

/*!@#$%^&*!@#$%^&*~~优美的分界线~~*&^%$#@!*&^%$#@!*/
signed main(){
	srand(time(0));
	n=read();
	ins(INT_MAX);
	ins(INT_MIN);
	while(n--) {
		int opt=read(),x=read();
		if(opt==1) ins(x);
		if(opt==2) del(x);
		if(opt==3) printf("%d\n",ask(x));
		if(opt==4) printf("%d\n",kth(root,x+1));
		if(opt==5) {
			int l,r;
			spilt_val(root,x-1,l,r);
			cout<<kth(l,T[l].siz)<<'\n';
			root=Merge(l,r);
		}
		if(opt==6) {
			int l,r;
			spilt_val(root,x,l,r);
			cout<<kth(r,1)<<'\n';
			root=Merge(l,r);
		}
	}
	return 0;
}

替罪羊

#include<bits/stdc++.h>
using namespace std;
int n,tot,root; 
const int N=1e5+5;
const double alpha=0.75;
inline int read(){
	int s=0,f=1; char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){s=(s<<3)+(s<<1)+(ch^48);ch=getchar();}
	return s*f;
}
struct node{
	#define ls tr[p].lc
	#define rs tr[p].rc 
	int lc,rc;
	int val,sz,cnt;//sz子树中数的个数
	int sn,tn;//sn总的节点个数,tn实际的节点个数
}tr[N<<1];
int tmp[N],tmp_n;//临时数组,用来储存不平衡的子树
void pushup(int p){//向上更新节点信息 
	tr[p].sz=tr[ls].sz+tr[rs].sz+tr[p].cnt;
	tr[p].sn=tr[ls].sn+tr[rs].sn+1;
	tr[p].tn=tr[ls].tn+tr[rs].tn+(tr[p].cnt?1:0);
}
int get_node(int x){//创建一个值为x的新节点 
	++tot;
	tr[tot]={0,0,x,1,1,1,1};
	return tot;
}
bool check(int p){//判断是否平衡 
	if(tr[ls].sn>tr[p].sn*alpha||tr[rs].sn>tr[p].sn*alpha) return 0;//若左右子树节点差的太多 
	if(tr[p].tn<tr[p].sn*alpha) return 0;//若已经被删除的节点太多 
	return 1;
}
//将子树压扁为数列
void flatt(int p){
	if(!p) return;
	flatt(ls);
	if(tr[p].cnt>0) tmp[++tmp_n]=p;//若该节点没被删出,将标号压入临时数组 
	flatt(rs);
} 
//将数列重新变为平衡的树
int build(int l,int r){
	if(l>r) return 0;
	int mid=l+r>>1;
	int p=tmp[mid];//将存储在数组中的标号取出 
	ls=build(l,mid-1);
	rs=build(mid+1,r);
	pushup(p);
	return p;
} 
//统一调用进行重建操作,注意要取址,因为要更改原节点的左右儿子关系 
void rebuild(int &p){
	tmp_n=0;
	flatt(p);
	if(p) p=build(1,tmp_n);
	else p=0;
} 
void ins(int &p,int x){
	if(!p){
		p=get_node(x);
		return;
	}
	if(tr[p].val>x) ins(tr[p].lc,x);
	else if(tr[p].val<x) ins(tr[p].rc,x);
	else tr[p].cnt++;
	pushup(p);
	if(!check(p)) rebuild(p);//若不平衡,则重构子树 
}
void del(int &p,int x){
	if(!p) return;
	if(tr[p].val>x) del(ls,x);
	else if(tr[p].val<x) del(rs,x);
	else tr[p].cnt--;
	pushup(p);
	if(!check(p)) rebuild(p);
}
int ask(int p,int x){
	if(!p)return 0;
	if(tr[p].val==x)return tr[ls].sz;
	if(tr[p].val>x)return ask(ls,x);
	return ask(rs,x)+tr[ls].sz+tr[p].cnt;
}
int kth(int p,int x){
	if(!p) return 0;
	int ans=tr[tr[p].lc].sz;
	if(ans>=x) return kth(ls,x); 
	else if(ans+tr[p].cnt>=x) return tr[p].val;
	else return kth(rs,x-ans-tr[p].cnt); 
}
int main() {
	n=read();
	ins(root,INT_MAX);
	ins(root,INT_MIN);
	while(n--){
		int opt=read(),x=read();
		if(opt==1) ins(root,x);
		if(opt==2) del(root,x);
		if(opt==3) printf("%d\n",ask(root,x));
		if(opt==4) printf("%d\n",kth(root,x+1));
		if(opt==5) printf("%d\n",kth(root,ask(root,x)));
		if(opt==6) printf("%d\n",kth(root,ask(root,x+1)+1)); 
	}
    return 0;
}

一般来说,我们可以用这个:pb_ds

操作:

tre.insert(x); //插入x;
tre.erase(x); //删除x;
tre.order_of_key(x); //返回x的排名;
tre.find_by_order(k); //返回第k小的值的迭代器;
tre.lower_bound(x); //返回第一个大于等于x的元素的迭代器;
tre.upper_bound(x); //返回第一个大于x的元素的迭代器;

tre.join(b); //树的合并;
tre.split(a,b); //树的分裂;

CODE:

#include <bits/stdc++.h>
#include <bits/extc++.h>
#define ll long long
using namespace std;
using namespace __gnu_pbds;
/*!@#$%^&*!@#$%^&*~~ Boundary Line ~~*&^%$#@!*&^%$#@!*/
int n;
tree<ll,null_type,less<ll>,rb_tree_tag,
tree_order_statistics_node_update> T;
/*!@#$%^&*!@#$%^&*~~ Boundary Line ~~*&^%$#@!*&^%$#@!*/

/*!@#$%^&*!@#$%^&*~~ Boundary Line ~~*&^%$#@!*&^%$#@!*/
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        ll op,x;cin>>op>>x;
        if(op==1) T.insert((x<<20)+i);
        else if(op==2) T.erase(T.lower_bound(x<<20));
        else if(op==3) cout<<T.order_of_key(x<<20)+1<<'\n';
        else if(op==4) cout<<(*T.find_by_order(x-1)>>20)<<'\n';
        else if(op==5) cout<<(*--T.lower_bound((x<<20))>>20)<<'\n';
        else if(op==6) cout<<(*T.upper_bound((x<<20)+n)>>20)<<'\n';
    }
    return 0;
}

总结

!!!这里并不包含 主席树CDQ分治

平衡树的插入,删除为其对应操作

时间复杂度 空间复杂度 常数 码量 适用性
分块 增删:\(O(sqrt(N))\) 查询:\(O(sqrt(N))\) \(\mathcal O(N+sqrt(N))\) 相对较少 处理 \(n\) 比较小,而操作比较复杂
树状数组 增删:\(O(log N)\) 查询: \(O(log N)\) \(\mathcal O(N)\) 极小 大多维护可差分信息,如果维护不可差分,时间复杂度为:\(\mathcal O (\log^2 N)\)
线段树 增删:\(O(log N)\) 查询:\(O(log N)\) \(\mathcal O(4N)\) or$ O\mathcal (Q log N)$ (动态开点) 中等 较少 维护:区间和,最值···权值线段树维护:前驱,后继···
替罪羊 增删:\(O(log N)\) 查询:\(O(log N)\) \(\mathcal O(N)\) 中等 中等 批量插入元素,查询元素
Head-Treap 增删:\(O(log N)\) 查询:\(O(log N)\) \(\mathcal O(N)\) 中等 较少 插入元素,查询元素
Splay 增删:\(\mathcal O(log N)\) 查询:\(\mathcal O (log N)\) \(\mathcal O(N)\) 中等 插入元素,查询元素,区间处理,维护
FHQ-Treap 增删:\(\mathcal O(log N)\) 查询:\(\mathcal O (log N)\) \(\mathcal O(N)\) 中等 批量,插入元素,查询元素,区间处理,可持久化,维护序列
posted @ 2025-11-08 08:17  hjm0703  阅读(11)  评论(0)    收藏  举报