其他分享
首页 > 其他分享> > 平衡树(Splay)

平衡树(Splay)

作者:互联网

平衡树

不同平衡树

有许多不同的平衡树

如:替罪羊树,AVI,红黑树,TreapFHQ-Treap (无旋Treap),SplaySBT

其中比较重点的是上述后四种

目前只学习了 SplayTreap

能够较为熟练的打出来的只有 Splay

有关 Splay

代码 (luoguP3396) :

#include<bits/stdc++.h>
using namespace std;

inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}

#define size six
#define next nex

const int N=2e5+5;

int n;
int size[N],son[N][2],f[N],cnt[N],val[N],tot,rt;

struct Splay{
	inline void maintain(int x){
		size[x]=size[son[x][0]]+size[son[x][1]]+cnt[x];
	}

	inline bool get(int x){
		return x==son[f[x]][1];
	}

	inline void rotate(int x){
		int y=f[x],z=f[y],k=get(x),w=son[x][k^1];
		son[y][k]=w,f[w]=y;
		son[z][get(y)]=x,f[x]=z;
		son[x][k^1]=y,f[y]=x;
		maintain(y),maintain(x);
	}
	
	inline void splay(int x,int pos){
		while(f[x]!=pos){
			int y=f[x],z=f[y];
			if(z!=pos){
				if(get(x)==get(y)) rotate(y);
				else rotate(x);
			}
			rotate(x);
		}
		if(pos==0) rt=x;
	}
	
	inline void insert(int x){
		int cur=rt,p=0;
		for(;cur&&val[cur]!=x;p=cur,cur=son[cur][x>val[cur]]);
		if(cur) ++cnt[cur];
		else{
			cur=++tot;
			if(p) son[p][x>val[p]]=cur;
			son[cur][0]=son[cur][1]=0;
			f[cur]=p,val[cur]=x;
			cnt[cur]=size[cur]=1;
		}
		splay(cur,0);
	}
	
	inline void find(int x){
		int cur=rt;
		for(;son[cur][x>val[cur]]&&x!=val[cur];cur=son[cur][x>val[cur]]);
		splay(cur,0);
	}
	
	inline int xth(int x){
		int cur=rt;
		while(1){
			if(son[cur][0]&&x<=size[son[cur][0]]) cur=son[cur][0];
			else if(x>size[son[cur][0]]+cnt[cur]){
				x-=size[son[cur][0]]+cnt[cur];
				cur=son[cur][1];
			}
			else{
				splay(cur,0);
				return cur;
			}
		}
	}
	
	inline int pre(int x){
		find(x);
		if(val[rt]<x) return rt;
		int cur=son[rt][0];
		while(son[cur][1]) cur=son[cur][1];
		return cur;
	}
	
	inline int next(int x){
		find(x);
		if(val[rt]>x) return rt;
		int cur=son[rt][1];
		while(son[cur][0]) cur=son[cur][0];
		return cur;
	}
	
	inline void del(int x){
		int pr=pre(x),suc=next(x);
		splay(pr,0),splay(suc,pr);
		int y=son[suc][0];
		if(cnt[y]>1){
			--cnt[y];
			splay(y,0);
		}
		else son[suc][0]=0;
		splay(suc,0);
	}
}t;
signed main(){
	n=read();
	t.insert(100000000);
	t.insert(-100000000);
	while(n--){
		int op=read(),x=read();
		if(op==1) t.insert(x);
		if(op==2) t.del(x);
		if(op==3){
			t.find(x);
			printf("%d\n",size[son[rt][0]]);
		}
		if(op==4) printf("%d\n",val[t.xth(x+1)]);
		if(op==5) printf("%d\n",val[t.pre(x)]);
		if(op==6) printf("%d\n",val[t.next(x)]);
	}
}

当然,之后可能会采用结构体的写法

Splay 是平衡树,它是通过 \(splay\) (伸展) 这一操作来维持平衡的

\(splay(x,pos)\) 表示通过不断 \(rotate\) 把 \(x\) 节点旋转到 \(pos\) 节点的儿子处

\(splay(x,0)\) 表示把 \(x\) 旋转到根

这样我们可以通过 \(splay(pre(val),0)\) \(splay(next(val),pre(val))\) 等一系列操作更方便的执行复杂的插入与修改

具体会在题目中阐述

Splay 有两点重要的地方:

个人比较喜欢 Splay 的原因:好学,方便,比 Treap 运用的范围更广

当然 FHQ-Treap 貌似也能维护序列 (可我不会)

Splay 习题

P3391 文艺平衡树

题意

给你一个长度为 \(n\) 的序列,序列的第 \(i\) 项初始为 \(i\)

会进行 \(m\) 次翻转序列中区间 \([l,r]\) 的操作

最后输出序列

满足 \(1\leq n,m\leq 10^5\)

思路

将序列每个点的位置存入平衡树中 (这样它的中序遍历就是最后的序列)

上述操作可以 \(insert\) 也可以递归建树 (像线段树一样)

翻转区间 \([l,r]\) 时我们 \(splay(l-1,0)\) \(splay(r+1,l-1)\)

这样区间 \([l,r]\) 就在 \(r+1\) 的左子树中

然后将 \(r+1\) 的左子树中所有点的左右子树交换就行

但是一次全交换会 T

所以考虑像线段树一样整一个翻转的 \(lazytag\)

同时在每次访问前 \(pushdown\) 更新

这样就可以保证复杂度

code

P2234 营业额统计

题意

有 \(n\) 天,告诉你每天的营业额 \(a_i\)

我们定义,一天的最小波动值 = \(\min\{|\text{该天以前某一天的营业额}-\text{该天营业额}|\}\)

特别地,第一天的最小波动值为第一天的营业额

求最小波动值之和

思路

没什么特别,Splay

code

P1486 郁闷的出纳员

题意

给你 \(n\) 条命令,\(\min\) 的值 (工资下界)

在初始时,可以认为公司里一个员工也没有。

思路

考虑到维护 A 操作比较复杂

搞一个增加值 \(delta\) 表示当前已经加过 \(delta\) 这么多工资

每次插入 \(x\) 的时候 \(insert(x-delta)\)

这样树内的每个值 \(val\) 实际上是 \(val+delta\)

由于需要删掉每个小于 \(min\) 的节点

每次删除操作时把大于等于 \(min-delta\) 的最小值 \(splay\) 到根,然后把根的左儿子变成 \(0\)

这样就完成了删除操作

注意这里调用 \(next(min-delta)\) 就要把 \(>\) 改成 \(\ge\),或者是调用 \(next(min-delta-1)\)

code

P2286 宠物收养场

题意

凡凡开了一间宠物收养场。收养场提供两种服务:收养被主人遗弃的宠物和让新的主人领养这些宠物。

被遗弃的宠物过多时,假若到来一个领养者,这个领养者希望领养的宠物的特点值为 \(a\),那么它将会领养一只目前未被领养的宠物中特点值最接近 \(a\) 的一只宠物。

(任何两只宠物的特点值都不可能是相同的,任何两个领养者的希望领养宠物的特点值也不可能是一样的)

如果存在两只宠物他们的特点值分别为 \(a-b\) 和 \(a+b\),那么领养者将会领养特点值为 \(a-b\) 的那只宠物。

收养宠物的人过多,假若到来一只被收养的宠物,能够领养它的领养者,是那个希望被领养宠物的特点值最接近该宠物特点值的领养者。

如果该宠物的特点值为a,存在两个领养者他们希望领养宠物的特点值分别为 \(a-b\) 和 \(a+b\),那么特点值为 \(a-b\) 的那个领养者将成功领养该宠物。

一个领养者领养了一个特点值为 \(a\) 的宠物,而它本身希望领养的宠物的特点值为 \(b\),那么这个领养者的不满意程度为 \(|a-b|\)。

给你 \(n\) 个 领养者和被收养宠物到来收养所的情况,请你计算所有收养了宠物的领养者的不满意程度的总和。

初始时,收养所里面既没有宠物,也没有领养者。

\(0<n\leq80000\)

思路

维护一棵 Splay 以及当前树内是宠物还是领养者

找前驱后继,若前驱后继都行则选前驱,然后删掉就行

code

P3850 书架

题意

有一个书架上有 \(N\) 本书,给你 \(M\) 本书以及要插入的位置 \(x\),将其插进去

\(Q\) 次询问,每次问你第 \(k\) 本书的名字

\(1 \leqslant N \leqslant 200\), \(1 \leqslant M \leqslant 10^5\), \(1 \leqslant Q \leqslant 10^4\)

思路

Splay 维护位置

每次插入到位置 \(x\) 就找到在树中排名为 \(x\) 的位置 \(a\) 并 \(splay(a,0)\)

找到在树中排名为 \(x-1\) 的位置 \(b\) 并 \(splay(b,a)\)

这样 \(b\) 的右儿子为空,我们让 \(x\) 为 \(b\) 的右儿子就完成了插入

每次查找直接问就行 (因为树中的排名就表示序列中的排名)

code

P3586 LOG

题意

维护一个长度为 \(n\) 的序列,一开始都是 \(0\),支持以下两种操作:

  1. U k a 将序列中第 \(k\) 个数修改为 \(a\)。
  2. Z c s 在这个序列上,每次选出 \(c\) 个正数,并将它们都减去 \(1\),询问能否进行 \(s\) 次操作。

每次询问独立,即每次询问不会对序列进行修改。

思路

操作 \(1\) 很容易

对于操作 \(2\) 分为大于等于 \(s\) 的数和小于 \(s\) 的数,让它们总贡献大于等于 \(c\cdot s\) 就行

假设大于等于 \(s\) 的数有 \(x\) 个,那么它们的贡献就是 \(x\cdot s\) (每一个数的贡献至多为 \(s\) )

而小于 \(s\) 的数的贡献就是它们的和 \(Sum\)

所以能进行 \(s\) 次操作的条件是: \(x\cdot s+Sum\ge c\cdot s\)

也就是 \(Sum\ge (c-x)\cdot s\)

因此在 Splay 中 \(maintain\) 的时候多维护一个东西 \(sum\) (这个东西的维护方式就和 \(size\) 一样)

设 \(pre(s)=y\),每次查询的时候 \(splay(y,0)\),那么就可以计算 \(Sum\) 和 \(x\) 了

\(Sum=sum[p]-sum[son[p][1]]\)

\(x=size[son[p][1]]]\)

注意:代码中 \(x=size[son[p][1]]-1\) 因为最开始插入了一个极大值防止越界

code

标签:splay,宠物,cur,val,领养,son,Splay,平衡
来源: https://www.cnblogs.com/into-qwq/p/16437085.html