平衡树(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) 表示通过不断 rotatex 节点旋转到 pos 节点的儿子处

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

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

具体会在题目中阐述

Splay 有两点重要的地方:

  • 它可以维护序列
  • 它是 LCT(Link Cut Tree) 的基础

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

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

Splay 习题#

P3391 文艺平衡树#

题意

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

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

最后输出序列

满足 1n,m105

思路

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

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

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

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

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

但是一次全交换会 T

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

同时在每次访问前 pushdown 更新

这样就可以保证复杂度

code

P2234 营业额统计#

题意

n 天,告诉你每天的营业额 ai

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

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

求最小波动值之和

思路

没什么特别,Splay

code

P1486 郁闷的出纳员#

题意

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

  • I k 新建一个工资档案,初始工资为 k。如果某员工的初始工资低于工资下界,他将立刻离开公司。

  • A k 把每位员工的工资加上 k

  • S k 把每位员工的工资扣除 k

  • F k 查询第 k 多的工资。

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

  • 0n3×1050min109

思路

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

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

每次插入 x 的时候 insert(xdelta)

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

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

每次删除操作时把大于等于 mindelta 的最小值 splay 到根,然后把根的左儿子变成 0

这样就完成了删除操作

注意这里调用 next(mindelta) 就要把 > 改成 ,或者是调用 next(mindelta1)

code

P2286 宠物收养场#

题意

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

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

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

如果存在两只宠物他们的特点值分别为 aba+b,那么领养者将会领养特点值为 ab 的那只宠物。

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

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

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

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

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

0<n80000

思路

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

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

code

P3850 书架#

题意

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

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

1N200, 1M105, 1Q104

思路

Splay 维护位置

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

找到在树中排名为 x1 的位置 bsplay(b,a)

这样 b 的右儿子为空,我们让 xb 的右儿子就完成了插入

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

code

P3586 LOG#

题意

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

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

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

思路

操作 1 很容易

对于操作 2 分为大于等于 s 的数和小于 s 的数,让它们总贡献大于等于 cs 就行

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

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

所以能进行 s 次操作的条件是: xs+Sumcs

也就是 Sum(cx)s

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

pre(s)=y,每次查询的时候 splay(y,0),那么就可以计算 Sumx

Sum=sum[p]sum[son[p][1]]

x=size[son[p][1]]]

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

code

作者:Into_qwq

出处:https://www.cnblogs.com/into-qwq/p/16437085.html

版权:本作品采用「qwq」许可协议进行许可。

posted @   Into_qwq  阅读(83)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示