【数据结构】Splay详解

引入

首先我们要知道一个东西叫二叉搜索树。
其定义如下:

  1. 空树是二叉搜索树。
  2. 若二叉搜索树的左子树不为空,则其左子树上所有点的附加权值均小于其根节点的值。
  3. 若二叉搜索树的右子树不为空,则其右子树上所有点的附加权值均大于其根节点的值。
  4. 二叉搜索树的左右子树均为二叉搜索树。

二叉搜索树上的基本操作所花费的时间与这棵树的高度成正比。对于一个有 n n n 个结点的二叉搜索树中,这些操作的最优时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

如图就是一颗典型的 BST(二叉查找树)
在这里插入图片描述
可是我们发现,如果树退化成一条链,那么时间复杂度将退化为 O ( n ) O(n) O(n),这是我们不能接受的,于是平衡树孕育而生,其核心就是维护一颗相对平衡的 BST。
本文将介绍Splay,虽然它并不能保证树一直是"平衡"的,但对于Splay的一系列操作,我们可以证明其每步操作的平摊复杂度都是 O ( log ⁡ n ) O(\log n) O(logn)。所以从某种意义上说,Splay也是一种平衡的二叉查找树。

Splay

旋转操作

下面参考 OI-WIKI的介绍。
在这里插入图片描述
注意,左右旋指的是向左或右旋转。
左旋为ZAG,右旋为ZIG
以下是一次标准旋转操作:
在这里插入图片描述
我们可以知道,旋转流程如下:
在这里插入图片描述

于是我们便可以写出 ZIG和ZAG函数,参考下列代码:
在这里插入图片描述
在这里插入图片描述
不过有时候为了方便表示,我们可以把两个旋转操作合并起来。
就成了 rotate(旋转)函数,以下是参考代码:

void rotate(int x){
		int y=fa[x],z=fa[y],id=son_(x);
		ch[y][id]=ch[x][id^1];
		if(ch[x][id^1])
			fa[ch[x][id^1]]=y;
		ch[x][id^1]=y;
		fa[y]=x;
		fa[x]=z;
		if(z)
			ch[z][y==ch[z][1]]=x;
		pushup(y);
		pushup(x);
	}

其中 son_( x x x)是判断 x x x 为父节点的左儿子还是右儿子,pushup为由下往上更新。

splay操作

这个操作可以说是Splay的核心操作之一,可以理解为把某个点通过旋转操作旋转到根节点。
那么如何将一个节点旋转到根节点呢?
首先有 6 6 6 种基本情况,见下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那么我们只需要不断重复执行旋转操作,即可旋转到根节点。
以下是参考代码:

void splay(int x) {
  for (int f = fa[x]; f = fa[x], f; rotate(x))
    if (fa[f]) 
    	rotate(get(x) == get(f) ? f : x);
  rt = x;
}

一些进阶
由于后面某些操作需要用到,所以我们对splay函数进行一些修改。
具体而言,我们引入一个参数 y y y,让splay把 x x x 旋转到 y y y 的儿子上。(当 y = 0 y=0 y=0 时将 x x x 旋转到根节点)
其实也没什么改动,见参考代码:

void splay(int x,int y){
	while(fa[x]!=y){
		if(fa[fa[x]]!=y){
			if(son_(fa[x])==son_(x))
				rotate(fa[x]);
			else
				rotate(x);
		}
		rotate(x);
	}
	if(!y)
		rt=x;
}

插入操作

在这里插入图片描述

解释一下:
二叉树的性质使得插入操作变得非常简易,具体而言,只要值比当前节点大,就往右子树找,小就往左子树找,一样就让计数器+1,如果找不到匹配的值就直接新建一个节点。
参考代码:

	void add(int k){
		if(!rt){
			rt=++idx;
			cnt[rt]++,val[rt]=k;
			pushup(rt);
			return ;
		}
		int x=rt,y=0;
		while(1){
			if(val[x]==k){
				cnt[x]++;
				pushup(x),pushup(y);
				splay(x,0);
				break;
			}
			y=x;
			x=ch[x][val[x]<k];
			if(!x){
				cnt[++idx]++,val[idx]=k;
				fa[idx]=y;
				ch[y][val[y]<k]=idx;
				pushup(idx);
				pushup(y);
				splay(idx,0);
				break;
			}
		}
	}

查询x排名

这个跟插入差不多,从根节点不断往下找,每次向右子树找时加上左子树的size+1,因为左子树和根的值一定比查询值小(BST的性质)。
具体详见代码:

	int x_rank(int k){
		int rk=0,x=rt;
		while(1){
			if(k<val[x])
				x=ch[x][0];
			else{
				rk+=sz[ch[x][0]];
				if(!x)
					return rk+1;
				if(k==val[x]){
					splay(x,0);
					return rk+1;
				}
				rk+=cnt[x];
				x=ch[x][1];
			}
		}
	}

查询排名为x

这个跟上面两个操作都差不多,不断往下找就行了。
看着代码,画画图也就能理解了。

	int kth(int k){
		int x=rt;
		while(1){
			if(ch[x][0]&&k<=sz[ch[x][0]])
				x=ch[x][0];
			else{
				k-=sz[ch[x][0]];
				if(k<=cnt[x]){
					splay(x,0);
					return val[x];
				}
				k-=cnt[x];
				x=ch[x][1];
			}
		}
	}

删除操作

在这里插入图片描述
这个就感性理解一下。
参考代码:

	void del(int k){
		x_rank(k);
		int x=rt,y=0;
		if(cnt[rt]>1)
			cnt[rt]--,pushup(rt);
		else if(!ch[rt][0]&&!ch[rt][1])
			clean(rt),rt=0;
		else if(!ch[rt][0]){
			rt=ch[rt][1];
			fa[rt]=0;
			clean(x);
		}
		else if(!ch[rt][1]){
			rt=ch[rt][0];
			fa[rt]=0;
			clean(x);
		}
		else{
			pre();
			fa[ch[x][1]]=rt;
			ch[rt][1]=ch[x][1];
			clean(x),pushup(rt);
		}
	}

或者还有一种方式,我们把 x x x 的前驱旋转到根节点,再把 x x x 的后继旋转到根节点的右子树上,这样根节点的右子树的左儿子即为目标节点,直接断开联系即为删除。
参考代码:

void del(int x){
	int l=kth(x-1),r=kth(r+1);
	splay(l,0),splay(r,l);
	fa[ch[r][0]]=0,ch[r][0]=0;
	pushup(r);
	pushup(l);
}

查询前驱/后继

这个可以先将这个节点插入,此时它在根节点,那么前驱就是它左子树中最右的点,后继就是它右子树中最左的点。
查询完我们在删除这个点即可。
参考代码:

	int pre(){
		int z=ch[rt][0];
		while(ch[z][1])
			z=ch[z][1];
		splay(z,0);
		return z;
	}
	int nxt(){
		int z=ch[rt][1];
		while(ch[z][0])
			z=ch[z][0];
		splay(z,0);
		return z;
	}

模板

综合上述操作,我们即可A掉洛谷模版题。
P3369 【模板】普通平衡树

题目概述:
在这里插入图片描述
参考代码:

struct Tr_splay{
	int fa[N],ch[N][2],sz[N],val[N],cnt[N];
	void pushup(int x){
		sz[x]=sz[ch[x][0]]+sz[ch[x][1]]+cnt[x];
	}
	void clean(int x){
		fa[x]=sz[x]=cnt[x]=val[x]=ch[x][0]=ch[x][1]=0;
	}
	bool son_(int x){
		return x==ch[fa[x]][1];
	}
	void rotate(int x){
		int y=fa[x],z=fa[y],id=son_(x);
		ch[y][id]=ch[x][id^1];
		if(ch[x][id^1])
			fa[ch[x][id^1]]=y;
		ch[x][id^1]=y;
		fa[y]=x;
		fa[x]=z;
		if(z)
			ch[z][y==ch[z][1]]=x;
		pushup(y);
		pushup(x);
	}
	void splay(int x,int y){
		while(fa[x]!=y){
			if(fa[fa[x]]!=y){
				if(son_(fa[x])==son_(x))
					rotate(fa[x]);
				else
					rotate(x);
			}
			rotate(x);
		}
		if(!y)
			rt=x;
	}
	int pre(){
		int z=ch[rt][0];
		while(ch[z][1])
			z=ch[z][1];
		splay(z,0);
		return z;
	}
	int nxt(){
		int z=ch[rt][1];
		while(ch[z][0])
			z=ch[z][0];
		splay(z,0);
		return z;
	}
	void add(int k){
		if(!rt){
			rt=++idx;
			cnt[rt]++,val[rt]=k;
			pushup(rt);
			return ;
		}
		int x=rt,y=0;
		while(1){
			if(val[x]==k){
				cnt[x]++;
				pushup(x),pushup(y);
				splay(x,0);
				break;
			}
			y=x;
			x=ch[x][val[x]<k];
			if(!x){
				cnt[++idx]++,val[idx]=k;
				fa[idx]=y;
				ch[y][val[y]<k]=idx;
				pushup(idx);
				pushup(y);
				splay(idx,0);
				break;
			}
		}
	}
	int x_rank(int k){
		int rk=0,x=rt;
		while(1){
			if(k<val[x])
				x=ch[x][0];
			else{
				rk+=sz[ch[x][0]];
				if(!x)
					return rk+1;
				if(k==val[x]){
					splay(x,0);
					return rk+1;
				}
				rk+=cnt[x];
				x=ch[x][1];
			}
		}
	}
	int kth(int k){
		int x=rt;
		while(1){
			if(ch[x][0]&&k<=sz[ch[x][0]])
				x=ch[x][0];
			else{
				k-=sz[ch[x][0]];
				if(k<=cnt[x]){
					splay(x,0);
					return val[x];
				}
				k-=cnt[x];
				x=ch[x][1];
			}
		}
	}
	void del(int k){
		x_rank(k);
		int x=rt,y=0;
		if(cnt[rt]>1)
			cnt[rt]--,pushup(rt);
		else if(!ch[rt][0]&&!ch[rt][1])
			clean(rt),rt=0;
		else if(!ch[rt][0]){
			rt=ch[rt][1];
			fa[rt]=0;
			clean(x);
		}
		else if(!ch[rt][1]){
			rt=ch[rt][0];
			fa[rt]=0;
			clean(x);
		}
		else{
			pre();
			fa[ch[x][1]]=rt;
			ch[rt][1]=ch[x][1];
			clean(x),pushup(rt);
		}
	}
}tree;
signed main(){
	IOS;
	cin>>m;
	while(m--){
		int x,y;
		cin>>x>>y;
		if(x==1)tree.add(y);
		if(x==2)tree.del(y);
		if(x==3)tree.add(y),cout<<tree.x_rank(y)<<"\n",tree.del(y);
		if(x==4)cout<<tree.kth(y)<<"\n";
		if(x==5)tree.add(y),cout<<tree.val[tree.pre()]<<"\n",tree.del(y);
		if(x==6)tree.add(y),cout<<tree.val[tree.nxt()]<<"\n",tree.del(y);
	}
	return 0;
}

Splay时间复杂度分析

这个蒟蒻不会,但可以参考 OI-WIKI的证明:
证明

进阶操作

截取区间

Splay还可应用到序列操作中,具体而言,如果我们需要对区间 [ l , r ] [l,r] [l,r]进行操作,我们只需要先将 l − 1 l-1 l1 弄到根节点,再把 r + 1 r+1 r+1 弄到根节点的右儿子上,那么它的左子树就是区间 [ l , r ] [l,r] [l,r]了。
参考代码:

	int split(int l,int r){
		l=kth(l-1),r=kth(r+1);
		splay(l,0);
		splay(r,l);
		return ch[r][0];
	}
	//返回区间[l,r]对应的子树的根节点

区间加,区间赋值,区间查询,区间最值

这个类似线段树,我们相应的维护标记,并写好pushdown即可。
区间加参考:

void pushadd(int x,int k){
	val[x]+=k;
	sum[x]+=k*sz[x];
	add[x]+=k;
}
void modify1(int l,int r,int k){
	int _=split(l,r);
	pushadd(_,0,k);
	pushup(r);
	pushup(l);
}

区间赋值参考:

void pushcov(int x,int k){
	val[x]=k;
	sum[x]=sz[x]*k;
	add[x]=0;
	cov[x]=1;
}
void modify(int l,int r,int k){
	int _=split(l,r);
	pushcov(_,k);
	pushup(r);
	pushup(l);
}

区间查询参考:

void ask_sum(int l,int r){
	int _=split(l,r);
	cout<<sum[_]<<"\n";
}

区间翻转

这个呢我们还是搞一个懒标记然后下传,注意各个标记之间的先后顺序。
参考代码:

	void change(int x){
		swap(ch[x][0],ch[x][1]);
		lazy[x]^=1;
	}
	void reverse(int l,int r){
		l=kth(l),r=kth(r+2);
		splay(l,0);
		splay(r,l);
		change(ch[ch[l][1]][0]);
	}

原序列整体插入

有时候题目会直接给我们一个初始序列,一个个插入过于麻烦,于是我们可以类似线段树直接建树。
参考代码:

	int create(int k){
		int x=top?rb[top--]:++ID;
		ch[x][0]=ch[x][1]=fa[x]=rev[x]=cov[x]=0;
		sz[x]=1;
		val[x]=mx[x]=sum[x]=k;
		lx[x]=rx[x]=max(0ll,k);
		return x;
	}
	一些毒瘤题卡空间,这样回收可以节省空间。
	int build(int l,int r,int *a){
		if(l>r)
			return 0;
		if(l==r)
			return create(a[l]);
		int mid=(l+r)>>1,x=create(a[mid]);
		ch[x][0]=build(l,mid-1,a);
		ch[x][1]=build(mid+1,r,a);
		fa[ch[x][0]]=fa[ch[x][1]]=x;
		pushup(x);
		return x;
	}
rt=build(1,n,a);

指定位置插入

这个可以参考查询排名为x的操作。
能看到这里说明你已经是大佬了,看着代码画画图即可理解吧。

	void add(int pos,int k){
		kth(pos);
		pushdown(rt);
		fa[ch[rt][0]]=++ID,ch[ID][0]=ch[rt][0];
		ch[rt][0]=ID,fa[ID]=rt;
		sz[ID]=1;
		val[ID]=sum[ID]=k;
		pushup(ID);
		pushup(rt);
	}

整体插入末尾

这个也比较抽象,类似于建一棵新的splay,然后合并。

	void insert(int pos,int len,int *a){
		int _=build(1,len,a);
		int y=kth(pos),x=kth(pos+1);
		splay(y,0);
		splay(x,y);
		ch[x][0]=_,fa[_]=x;
		pushup(x);
		pushup(y);
	}

区间最大子段和

参考线段树,我们维护3个标记:
lx:从左起的最大子段和
mx:整个区间的最大子段和
rx:从右起的最大子段和
参考代码:(由于同时维护区间赋值和区间翻转,代码比较抽象)

	void pushup(int x){
		sz[x]=sz[ch[x][0]]+sz[ch[x][1]]+1;
		sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+val[x];
		lx[x]=max(lx[ch[x][0]],sum[ch[x][0]]+val[x]+lx[ch[x][1]]);
		rx[x]=max(rx[ch[x][1]],sum[ch[x][1]]+val[x]+rx[ch[x][0]]);
		mx[x]=max(max(mx[ch[x][0]],mx[ch[x][1]]),rx[ch[x][0]]+val[x]+lx[ch[x][1]]);
	}
	void pushdown(int x){
		if(cov[x]){
			if(ch[x][0])val[ch[x][0]]=val[x],cov[ch[x][0]]=1,sum[ch[x][0]]=val[x]*sz[ch[x][0]];
			if(ch[x][1])val[ch[x][1]]=val[x],cov[ch[x][1]]=1,sum[ch[x][1]]=val[x]*sz[ch[x][1]];
			if(val[x]>0){
				if(ch[x][0])lx[ch[x][0]]=rx[ch[x][0]]=mx[ch[x][0]]=sum[ch[x][0]];
				if(ch[x][1])lx[ch[x][1]]=rx[ch[x][1]]=mx[ch[x][1]]=sum[ch[x][1]];
			}
			else{
				if(ch[x][0])lx[ch[x][0]]=rx[ch[x][0]]=0,mx[ch[x][0]]=val[x];
				if(ch[x][1])lx[ch[x][1]]=rx[ch[x][1]]=0,mx[ch[x][1]]=val[x];
			}
			cov[x]=0;
		}
		if(rev[x]){
			if(ch[x][0])
				rev[ch[x][0]]^=1,swap(ch[ch[x][0]][0],ch[ch[x][0]][1]),swap(lx[ch[x][0]],rx[ch[x][0]]);
			if(ch[x][1])
				rev[ch[x][1]]^=1,swap(ch[ch[x][1]][0],ch[ch[x][1]][1]),swap(lx[ch[x][1]],rx[ch[x][1]]);
			rev[x]=0;
		}
	}
	void ask_max_sum(){
		cout<<mx[rt]<<"\n";
	}

一些好题

P2042
P4008
P6707

参考文献

  1. OI-WIKI
  2. 伸展树的基本操作和应用——杨思雨
  3. 各位大佬的博客和题解

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 05:58:02       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 05:58:02       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 05:58:02       57 阅读
  4. Python语言-面向对象

    2024-07-16 05:58:02       68 阅读

热门阅读

  1. Redis的哨兵和集群实现高可用

    2024-07-16 05:58:02       23 阅读
  2. Go:函数

    2024-07-16 05:58:02       21 阅读
  3. 在Delphi中使用ATTACH语句合并SQLite数据库

    2024-07-16 05:58:02       22 阅读
  4. Log4j2原理及应用详解(二)

    2024-07-16 05:58:02       20 阅读
  5. 在Ubuntu 18.04上安装和保护phpMyAdmin的方法

    2024-07-16 05:58:02       22 阅读
  6. 66.函数指针和回调函数

    2024-07-16 05:58:02       23 阅读
  7. MySQL第七次作业

    2024-07-16 05:58:02       24 阅读
  8. 机器学习与神经网络之间的关系 --九五小庞

    2024-07-16 05:58:02       22 阅读
  9. 面试题011-数据库-MySQL(事物+锁)

    2024-07-16 05:58:02       31 阅读
  10. Makefile 自动化变量以及模式匹配

    2024-07-16 05:58:02       25 阅读
  11. 云原生、Serverless、微服务概念

    2024-07-16 05:58:02       30 阅读
  12. x264 编码过程中视频相关数据流转分析

    2024-07-16 05:58:02       23 阅读
  13. Spring 如何解决循环依赖问题

    2024-07-16 05:58:02       26 阅读