[学习笔记] 伸展树splay详解+全套模板+例题[Luogu P3369 【模板】普通平衡树]

声明一下,许多代码的注解都在模板代码里面写了的,所以正文可能不会很多
其次是 s p l a y splay splay很多操作 t r e a p treap treap我都已经详解过了,只需要掌握不一样的旋转板块即可

引入概念

在这之前大家要了解二叉搜索树或者treap再或者非旋treap,也可以不了解,我会再次尽全力详细的给大家讲懵splay
在这里插入图片描述

二叉搜索树:是一种数据结构,每个点都存有各自的键值,按中序遍历这棵树,按键值生成的序列是有序的

显而易见对于给定的序列 n n n,它的二叉搜索树不是唯一的
煮个栗子: 1   2   3   4   5 1\ 2\ 3\ 4\ 5 1 2 3 4 5,就能画出很多不一样的二叉搜索树
在这里插入图片描述


伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在 O ( l o g n ) O(logn) O(logn)内完成插入、查找和删除操作
它由丹尼尔·斯立特Daniel Sleator 和 罗伯特·恩卓·塔扬Robert Endre Tarjan在1985年发明的

在伸展树上的一般操作都基于伸展操作:
假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置
于是想到设计一个简单方法:
在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方
伸展树应运而生:
伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去
它的优势在于不需要记录用于平衡树的冗余信息

假设想要对一个二叉查找树执行一系列的查找操作
为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置
于是想到设计一个简单方法:
在每次查找之后对树进行重构,把被查找的条目搬移到离树根近一些的地方
splay tree应运而生。splay tree是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去

———————百度百科老师亲身授课,讲懵一群中华少年

这张图太好看了,忍不住盗过来
在这里插入图片描述
在这里插入图片描述


重点的就是模板,模板的原理会在该模板板块介绍,不要慌~~

全套模板


变量声明

我用的是结构体 t r e e tree tree,方便学习 L C T LCT LCT(暴露了) 后面的封装
t r e e [ i ] . v a l tree[i].val tree[i].val:表示该点的值
t r e e [ i ] . c n t tree[i].cnt tree[i].cnt:表示该点在树上的出现次数
t r e e [ i ] . s i z tree[i].siz tree[i].siz:表示该点的子树大小,包括自己在内
t r e e [ i ] . f tree[i].f tree[i].f:表示该点的爸爸(诶真乖
t r e e [ i ] . s o n [ 2 ] tree[i].son[2] tree[i].son[2]:表示该点的两个儿子: s o n [ 0 ] son[0] son[0]左儿子, s o n [ 1 ] son[1] son[1]右儿子


这个没有什么值得讲的,不同的题肯定会有添加或更改,比如最大值就应该写成
t r e e [ x ] . m a x x = m a x ( t r e e [ t r e e [ x ] . s o n [ 0 ] ] . m a x x , t r e e [ t r e e [ x ] . s o n [ 1 ] ] . m a x x , t r e e [ x ] . v a l ) tree[x].maxx = max(tree[tree[x].son[0]].maxx,tree[tree[x].son[1]].maxx,tree[x].val) tree[x].maxx=max(tree[tree[x].son[0]].maxx,tree[tree[x].son[1]].maxx,tree[x].val)
这里以求和为例

update

void update ( int x ) {
	tree[x].siz = tree[tree[x].son[0]].siz + tree[tree[x].son[1]].siz + tree[x].cnt;
}

rotate旋转

t r e a p treap treap期间我们了解了单旋转(只旋一次),但是 s p l a y splay splay则是用双旋
接着因为是二叉树,双旋就分为了两种情况,直线型旋转和折线型旋转


直线型旋转,即三点成一条直线
在这里插入图片描述
这种情况的旋转规则:先旋转父亲,再旋转自己
在这里插入图片描述


折线型旋转
在这里插入图片描述
这种情况的旋转规则:旋转完自己,再旋转自己(自转两次)
在这里插入图片描述


总结一张图:
在这里插入图片描述

void rotate ( int x ) {//x是要旋转的点 
	int fa = tree[x].f;//x的父亲(father缩写) 
	int Gfa = tree[fa].f;//x的祖父/fa的父亲(grandfather缩写(*^__^*))
	int k = ( tree[fa].son[1] == x );//x是fa的哪一个儿子 0左儿子 1右儿子
	if( Gfa) tree[Gfa].son[tree[Gfa].son[1] == fa] = x;//儿子非要当爹 取代了爹原来在祖父下的位置
	tree[x].f = Gfa; 
	tree[fa].son[k] = tree[x].son[k ^ 1];
	if( tree[x].son[k ^ 1] ) tree[tree[x].son[k ^ 1]].f = fa;
	tree[x].son[k ^ 1] = fa;
	tree[fa].f = x;
	update ( fa );//别忘了更新信息
	update ( x );
}//0^1=1 1^1=0 其实也可以用取反(!)代替 

splay操作

我们使用双旋的做法,因为如果单旋将 x x x旋到想要的位置,毒瘤会卡到我们 n 2 n^2 n2
那么如果想旋转到根的话,可以给第二个参数传0

void splay ( int x, int goal ) {//将x旋转到goal的儿子 如果goal是0意味着将x转到根
	while ( tree[x].f != goal ) {
		int fa = tree[x].f, Gfa = tree[fa].f;
		if ( Gfa != goal )//如果fa不是根节点就是两类(直线 折线)旋转
			( ( tree[Gfa].son[0] == fa ) ^ ( tree[fa].son[0] == x ) ) ? rotate ( x ) : rotate ( fa );
		//有点技巧但也很好理解 前两坨^=0就是直线的意思
		rotate ( x );
	}
	if ( ! goal )
		root = x;//如果goal是0 将根节点更新为x
}

insert插入

先用个动图直观感受一下
在这里插入图片描述
t r e a p treap treap是孪生兄弟,从根开始,根据值的大小比较判断是往左走( x < t r e e [ r o o t ] . v a l x<tree[root].val x<tree[root].val)还是往右走( x > t r e e [ r o o t ] . v a l x>tree[root].val x>tree[root].val)

void insert ( int x ) {
	int u = root, fa = 0;//当前位置u及u的父节点f
	while ( u && tree[u].val != x ) {//仍有点且并未移动到想要的值 
		fa = u;
		u = tree[u].son[x > tree[u].val];//x大于当前点的值就在右儿子里面找 否则向左找 
	}
	if ( u ) //已经建过x这个值的位置了 
		tree[u].cnt ++;
	else {
		u = ++ Size;//新节点的位置 
		if ( fa ) 
			tree[fa].son[x > tree[fa].val] = u;
		tree[u].son[0] = tree[u].son[1] = 0;//新点目前肯定没有儿子
		tree[u].val = x;
		tree[u].f = fa;
		tree[u].cnt = tree[u].siz = 1;
	}
	splay ( u, 0 );//把当前位置移到根保证结构平衡 因为前面更改了子树大小必须splay去update保证siz的正确 
}

delete删除

思路是首先分别找到 x x x的前驱 p 1 p1 p1和后继 p 2 p2 p2,那么在当前树上就满足 p 1 < x < p 2 p1<x<p2 p1<x<p2并且中间没有其它数
很妙的就是我们把 p 1 p1 p1旋转到根,此时所有值比 p 1 p1 p1的都在右子树,然后把 p 2 p2 p2旋转到 p 1 p1 p1的儿子处,此时 p 2 p2 p2的左儿子就是 x x x且只有一个,因为 p 2 p2 p2的左子树要满足 > p 1 >p1 >p1 < p 2 <p2 <p2,显而易见因为定义这里面只能插 x x x,那么直接对 p 2 p2 p2的左子树进行操作即可

void Delete ( int x ) {
	int pre = PreSuf ( x, 0 ), suf = PreSuf ( x, 1 );
	splay ( pre, 0 );
	splay ( suf, pre );//pre有可能为0 但这个时候suf就应该旋转到根
	int u = tree[suf].son[0];
	if ( tree[u].cnt > 1 ) {
		tree[u].cnt --;
		splay ( u, 0 );
	}
	else
		tree[suf].son[0] = 0, splay( suf, 0 );
}

查找x的位置

我相信给个图,大家就懂了
在这里插入图片描述
在这里插入图片描述
与插入是一个意思,此处就不过多解释

void find ( int x ) {//查找x的位置并旋转到根节点 
	if ( ! root ) //树是空的
		return;
	int u = root;
	while ( tree[u].son[x > tree[u].val] && x != tree[u].val )//存在儿子并且当前点不是我们要找的 
		u = tree[u].son[x > tree[u].val];
	splay ( u, 0 ); 
}

查找第k大

不要多说废话了,不理解可以移步上面的 t r e a p treap treap讲解

int findkth ( int x ) {
	if ( tree[root].siz < x )//整棵树的大小都没有k即不存在 
		return -1;
	int u = root;
	while ( 1 ) {
		if ( x <= tree[tree[u].son[0]].siz )
			u = tree[u].son[0];
		else if ( x <= tree[u].cnt + tree[tree[u].son[0]].siz )
				return u;
			else {
				x -= ( tree[tree[u].son[0]].siz + tree[u].cnt );
				u = tree[u].son[1];
			}
	}
}

前驱/后继

前驱后继的思路很妙,我们以前驱为例,把 x x x旋到根,那么左子树就是比 x x x小的,然后就在左儿子里面一直往右儿子走, l i k e   t h i s ↓ like\ this↓ like this
在这里插入图片描述
但是如果树上没有我们要找的 x x x,怎么办呢,这个时候的树根究竟是什么,根据我们 f i n d find find的原理写法,可以知道我们一定是找的最接近于 x x x的值,不是它的前驱就是它的后继,那么这个时候根就有可能是答案
我们就在 f i n d find find后加入两个特判

int PreSuf ( int x, int f ) {//f=0找前驱 f=1找后继 
	find ( x );//查找后因为splay此时树根就是要查询节点
	if ( tree[root].val > x && f )//如果当前节点的值大于x并且要查找的是后继因为find原因可以直接返回了 
		return root;
	if ( tree[root].val < x && ! f )//与找后继同理 
		return root;
	int u = tree[root].son[f];
	if ( ! u )
		return 0;
	while ( tree[u].son[f ^ 1] )
		u = tree[u].son[f ^ 1];
	return u;
}

极小值-inf和极大值inf的作用

在没看到这个之前,如果你就拿着模板跑了,恭喜你流失了一天甚至更多的青春
因为上述模板都是在插入了哨兵的前提下才能运行的接下来让本蒟蒻来给你错误的讲讲哨兵的优秀

如果有哨兵存在,那么这些点永远都不会是死在最前面或者死的时候垫在最下面,就帮助我们少考虑很多边界,我昨天没有加哨兵,不停地补刀做手术,还是千疮百孔,病很多都是并发症,医不过来,加了个哨兵,自己就好了

最后简单提一下封装的好处,显然就是整个在一坨,方便整体移动和调试。代码分层也很清晰。可以用结构体

struct node {
	里面放所有splay的操作
}T;
调用函数需要写成 T.insert() 之类的

还可以

namespace splay {
	里面放所有splay操作
}
调用函数需要写成 splay :: insert() 之类的

通常是题目解法涉及到多种算法时,常按算法将各自模板进行封装。这样你可以很清楚地知道某个函数是属于哪一层的算法。

例题:P3369 【模板】普通平衡树

题目

送你离开千里之外

code

#include <cstdio>
#define maxn 100005
#define INF 0x7f7f7f7f
struct node {
	int f, cnt, val, siz, son[2];
}tree[maxn];
int n, Size, root;

void update ( int x ) {
	tree[x].siz = tree[tree[x].son[0]].siz + tree[tree[x].son[1]].siz + tree[x].cnt;
}

void rotate ( int x ) { 
	int fa = tree[x].f; 
	int Gfa = tree[fa].f;
	int k = ( tree[fa].son[1] == x );
	tree[Gfa].son[tree[Gfa].son[1] == fa] = x;
	tree[x].f = Gfa; 
	tree[fa].son[k] = tree[x].son[k ^ 1];
	tree[tree[x].son[k ^ 1]].f = fa;
	tree[x].son[k ^ 1] = fa;
	tree[fa].f = x;
	update ( fa );
	update ( x );
}

void splay ( int x, int goal ) {
	while ( tree[x].f != goal ) {
		int fa = tree[x].f, Gfa = tree[fa].f;
		if ( Gfa != goal )
			( ( tree[Gfa].son[0] == fa ) ^ ( tree[fa].son[0] == x ) ) ? rotate ( x ) : rotate ( fa );
		rotate ( x );
	}
	if ( ! goal )
		root = x;
}

void insert ( int x ) {
	int u = root, fa = 0;
	while ( u && tree[u].val != x ) {
		fa = u;
		u = tree[u].son[x > tree[u].val]; 
	}
	if ( u ) 
		tree[u].cnt ++;
	else {
		u = ++ Size; 
		if ( fa ) 
			tree[fa].son[x > tree[fa].val] = u;
		tree[u].son[0] = tree[u].son[1] = 0;
		tree[u].val = x;
		tree[u].f = fa;
		tree[u].cnt = tree[u].siz = 1;
	}
	splay ( u, 0 );
}

void find ( int x ) {
	if ( ! root )
		return;
	int u = root;
	while ( tree[u].son[x > tree[u].val] && x != tree[u].val )
		u = tree[u].son[x > tree[u].val];
	splay ( u, 0 ); 
}

int PreSuf ( int x, int f ) { 
	find ( x );
	if ( tree[root].val > x && f )
		return root;
	if ( tree[root].val < x && ! f )
		return root;
	int u = tree[root].son[f];
	if ( ! u )
		return 0;
	while ( tree[u].son[f ^ 1] )
		u = tree[u].son[f ^ 1];
	return u;
}

void Delete ( int x ) {
	int pre = PreSuf ( x, 0 ), suf = PreSuf ( x, 1 );
	splay ( pre, 0 );
	splay ( suf, pre );
	int u = tree[suf].son[0];
	if ( tree[u].cnt > 1 ) {
		tree[u].cnt --;
		splay ( u, 0 );
	}
	else
		tree[suf].son[0] = 0;
}

int findkth ( int x ) {
	if ( tree[root].siz < x )
		return -1;
	int u = root;
	while ( 1 ) {
		if ( x <= tree[tree[u].son[0]].siz )
			u = tree[u].son[0];
		else if ( x <= tree[u].cnt + tree[tree[u].son[0]].siz )
				return u;
			else {
				x -= ( tree[tree[u].son[0]].siz + tree[u].cnt );
				u = tree[u].son[1];
			}
	}
}

int main() {
	insert ( INF );
	insert ( -INF );
	scanf ( "%d", &n );
	int opt, x;
	for ( int i = 1;i <= n;i ++ ) {
		scanf ( "%d %d", &opt, &x );
		switch ( opt ) {
			case 1 : insert ( x ); break;
			case 2 : Delete ( x ); break;
			case 3 : {
				find ( x );
				printf ( "%d\n", tree[tree[root].son[0]].siz );
				break;
			}
			case 4 : {
				int u = findkth ( x + 1 );
				printf ( "%d\n", tree[u].val );
				break;
			}
			case 5 : {
				int u = PreSuf ( x, 0 );
				printf ( "%d\n", tree[u].val );
				break;
			}
			case 6 : {
				int u = PreSuf ( x, 1 );
				printf ( "%d\n", tree[u].val );
				break;
			}
		}
	}
	return 0;
}
#include <cstdio>
#define maxn 100005
#define INF 0x7f7f7f7f
struct SplayTree {
	struct node {
		int f, cnt, val, siz, son[2];
		void init ( int Val, int fa ) {
			val = Val;
			cnt = siz = 1;
			f = fa;
			son[0] = son[1] = 0;
		}
	}tree[maxn];
	int root, Size;
	
	void update ( int x ) {
		tree[x].siz = tree[tree[x].son[0]].siz + tree[tree[x].son[1]].siz + tree[x].cnt;
	}
	
	void rotate ( int x ) { 
		int fa = tree[x].f; 
		int Gfa = tree[fa].f;
		int k = ( tree[fa].son[1] == x );
		tree[Gfa].son[tree[Gfa].son[1] == fa] = x;
		tree[x].f = Gfa; 
		tree[fa].son[k] = tree[x].son[k ^ 1];
		tree[tree[x].son[k ^ 1]].f = fa;
		tree[x].son[k ^ 1] = fa;
		tree[fa].f = x;
		update ( fa );
		update ( x );
	}
	
	void splay ( int x, int goal ) {
		while ( tree[x].f != goal ) {
			int fa = tree[x].f, Gfa = tree[fa].f;
			if ( Gfa != goal )
				( ( tree[Gfa].son[0] == fa ) ^ ( tree[fa].son[0] == x ) ) ? rotate ( x ) : rotate ( fa );
			rotate ( x );
		}
		if ( ! goal )
			root = x;
	}
	
	void insert ( int x ) {
		int u = root, fa = 0;
		while ( u && tree[u].val != x ) {
			fa = u;
			u = tree[u].son[x > tree[u].val]; 
		}
		if ( u ) 
			tree[u].cnt ++;
		else {
			u = ++ Size; 
			if ( fa ) 
				tree[fa].son[x > tree[fa].val] = u;
			tree[u].son[0] = tree[u].son[1] = 0;
			tree[u].val = x;
			tree[u].f = fa;
			tree[u].cnt = tree[u].siz = 1;
		}
		splay ( u, 0 );
	}
	
	void find ( int x ) {
		if ( ! root )
			return;
		int u = root;
		while ( tree[u].son[x > tree[u].val] && x != tree[u].val )
			u = tree[u].son[x > tree[u].val];
		splay ( u, 0 ); 
	}
	
	int PreSuf ( int x, int f ) { 
		find ( x );
		if ( tree[root].val > x && f )
			return root;
		if ( tree[root].val < x && ! f )
			return root;
		int u = tree[root].son[f];
		if ( ! u )
			return 0;
		while ( tree[u].son[f ^ 1] )
			u = tree[u].son[f ^ 1];
		return u;
	}
	
	void Delete ( int x ) {
		int pre = PreSuf ( x, 0 ), suf = PreSuf ( x, 1 );
		splay ( pre, 0 );
		splay ( suf, pre );
		int u = tree[suf].son[0];
		if ( tree[u].cnt > 1 ) {
			tree[u].cnt --;
			splay ( u, 0 );
		}
		else
			tree[suf].son[0] = 0;
	}
	
	int findkth ( int x ) {
		if ( tree[root].siz < x )
			return -1;
		int u = root;
		while ( 1 ) {
			if ( x <= tree[tree[u].son[0]].siz )
				u = tree[u].son[0];
			else if ( x <= tree[u].cnt + tree[tree[u].son[0]].siz )
					return u;
				else {
					x -= ( tree[tree[u].son[0]].siz + tree[u].cnt );
					u = tree[u].son[1];
				}
		}
	}
	
}T;
int n;

int main() {
	T.insert ( -INF );
	T.insert ( INF );
	scanf ( "%d", &n );
	int opt, x;
	for ( int i = 1;i <= n;i ++ ) {
		scanf ( "%d %d", &opt, &x );
		switch ( opt ) {
			case 1 : T.insert ( x ); break;
			case 2 : T.Delete ( x ); break;
			case 3 : {
				T.find ( x );
				printf ( "%d\n", T.tree[T.tree[T.root].son[0]].siz );
				break;
			}
			case 4 : {
				int u = T.findkth ( x + 1 );
				printf ( "%d\n", T.tree[u].val );
				break;
			}
			case 5 : {
				int u = T.PreSuf ( x, 0 );
				printf ( "%d\n", T.tree[u].val );
				break;
			}
			case 6 : {
				int u = T.PreSuf ( x, 1 );
				printf ( "%d\n", T.tree[u].val );
				break;
			}
		}
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值