Gokix

一言(ヒトコト)

关注我

Splay 学习笔记

$$天を統べ、地を平らげる・・・万象すべて、地に平伏すがよ!$$

前置知识:二叉查找树 BST。

满足对于每个节点,其左子树的所有结点都比自己小,右子树的所有节点都比自己大。

性质:树的中序遍历相当于把所有数从小到大排序。

模板:P5076 【深基16.例7】普通二叉树(简化版)


极端情况下,二叉查找树可能成为一条链。若每次都在链的最底端做操作,那时间复杂度就退化成暴力了。

所以我们需要平衡操作来使二叉查找树“平衡”,“平衡”的二叉查找树叫做平衡树。平衡的定义因平衡树而异,但总之不会让每一次查询的深度都特别低。

本 Blog 主要复习 Splay,即伸展树,由 Daniel Sleator 和 Robert Endre Tarjan 在 1985 年发明。

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


变量声明:

\(c_{u,0/1}\):节点 \(u\) 的左儿子(0)或右儿子(1)的编号。

\(f_u\):节点 \(u\) 的父节点的编号。

\(w_u\):节点 \(u\) 的权值。

\(s_u\):节点 \(u\) 的子树大小(包括自己)。

\(cnt_u\):权值为 \(w_u\) 的数的个数。

\(rt\):树根编号。

注意到 Splay 的儿子存储方式与一般的方法不同。这么存储的原因是它在判断父子的左右关系的时候可以省略大段的 if 判断,这一点可以在下面的讲解有很好的体现。


下面开始函数的讲解:

1. chk(u):返回节点 u 是 f[u] 的左儿子(0)还是右儿子(1)
bool chk(long long x){
	return c[f[x]][1]==x;
}

这个感觉没有什么好讲的,自己手玩一下就好了。

chk 函数在之后会使代码变得简洁。


2. refresh(u):维护 s[u] 的大小
void refresh(long long x){
	s[x]=s[c[x][0]]+s[c[x][1]]+cnt[x];
}

这个感觉也没什么好讲的。


3. rotate(u):单旋,u 代替 f[u] 的位置 
void rotate(long long x){
	long long y=f[x],z=f[y],k=chk(x);
	c[y][k]=c[x][k^1],f[c[x][k^1]]=y;
	c[z][chk(y)]=x,f[x]=z;
	c[x][k^1]=y,f[y]=x;
	refresh(y),refresh(x);
} 

Splay 重要操作之一。

我们先看这个两个图:

我们要把节点 \(x\) 绕着节点 \(y\) 左旋或右旋无非就以上这2种情况。

\(z\)\(y\) 的父亲(\(x\) 的爷爷),其余的编号用法同上面的图。

rotate 分三步:

  1. \(B\)\(y\) 建立父子关系。

  2. \(z\)\(x\) 建立父子关系。

  3. \(x\)\(y\) 建立父子关系。

说白了 rotate 其实就是 \(x\) 代替了 \(y\) 的位置,而在此过程中:\(x\) 的与 \(x\) 作为 \(y\) 的儿子左右性相反的儿子(\(B\))要成为 \(y\)\(x\) 作为 \(y\) 的儿子左右性的儿子,\(y\) 要成为 \(x\) 与原 \(x\) 作为 \(y\) 的儿子左右性相反的儿子,\(x\) 作为 \(z\) 与原 \(y\) 作为 \(z\) 儿子的左右性相同的儿子(\(x\) 接替了 \(y\) 的位置)。

一开始不大好理解,写的时候画个图。

void rotate(long long x){
	long long y=f[x],z=f[y],k=chk(x);
	c[y][k]=c[x][k^1],f[c[x][k^1]]=y;
	c[z][chk(y)]=x,f[x]=z;
	c[x][k^1]=y,f[y]=x;
	refresh(y),refresh(x);
} 

4.splay(u,goal):双旋(伸展),u 和 f[u] 一起转直至到 goal 的儿子

Splay 重要操作之二。

双旋是 Splay 的特有操作(你看它都以 DS 自己的名字命名了)。

双旋出现的原因是因为如果只有单旋有可能树高还是很高,如图:

你发现单旋了很多次但是树高没变,相当于 rotate 成了废物。

所以引入双旋:在 \(y\) 成为 \(x\) 儿子的同时,\(x\) 代替了 \(z\) 的位置。

或者你可以理解为 \(x\)\(y\) 绕着 \(z\) 旋转。

双旋有两种情况:

一字形:先 rotate(y)rotate(x)

之字形:先 rotate(x)rotate(x)

如何判断究竟是一字形还是之字形呢?

简单,一字形就是 \(y\) 作为 \(z\) 的儿子的左右性与 \(x\) 作为 \(y\) 的儿子的左右性相同,反之,之字形就是不同。

所以只需要判断 chk(x) \(\operatorname{xor}\) chk(y) 的值就好了。

void splay(long long x,long long goal){
	if(!goal) rt=x;
	long long y,z;
	while(f[x]!=goal){
		y=f[x],z=f[y];
		if(z!=goal) chk(x)^chk(y)?rotate(x):rotate(y);
		rotate(x);
	}
}

还有就是如果我想把节点 \(x\) 转到根上,只需要 splay(x,0) 即可。


5.add(x):申请一个新建节点的空间,并把其权值赋值为 x
long long add(long long x){
	w[++tot]=x;
	cnt[tot]=s[tot]=1;
	f[tot]=c[tot][0]=c[tot][1]=0;
	return tot;
}

没啥好讲的,至于要返回一个 \(tot\) 的原因——我乐意(也就是说你可以不写,但是这样写后面可以省一些码量)。


6.insert(x):在树中插入权值为 x 的节点

思路很简单。从根开始,不断决定向当前节点 \(p\) 的左儿子还是右儿子走,同时记录当前节点的父亲 \(fp\).如果找到与插入权值大小相同的节点,那直接 \(cnt_p++\);否则新申请一个空间,把它作为 \(fp\) 的儿子。

为维持平衡,把新建的节点 splay 到根。这一点是 Splay 很多操作的特点,或者说是 Splay 本身的特点。

void insert(long long x){
	long long p=rt,fp=f[p];
	while(p && w[p]!=x){
		fp=p;
		p=c[p][x>w[p]];
	}
	if(!p){
		p=add(x);
		if(fp)
			c[fp][x>w[fp]]=p;
		f[p]=fp;
	}
	else cnt[p]++;
	splay(p,0);
}

7.find(x):若权值为 x 的节点存在,则把它转到根。若权值为 x 的节点不存在,则它的前驱或者后继会被转到根。

这个函数看上去非常奇怪,但它是 Splay 许多操作的基础(如:查前驱、查后继、查排名)。

实现的思路和 insert 差不多,这里就不再解释了。

void find(long long x){
	if(!rt) return;
	long long p=rt;
	while(w[p]!=x && c[p][x>w[p]]) 
		p=c[p][x>w[p]];
	splay(p,0);
}

8.pre(x):求权值 x 的前驱节点

首先 find(x)

这时候如果根比 \(x\) 小,那就说明是 \(x\) 的前驱被转到了根上,直接输出。

反之我们需要找根的左子树上最大的值。那么查询节点 \(p\)\(rt\) 的左儿子开始,能向右走就向右,直至不能走为止,则此时就是答案节点。

long long pre(long long x){
	find(x);
	if(w[rt]<x) return rt;
	long long p=c[rt][0];
	while(c[p][1]) p=c[p][1];
	return p;
}

9.suf(x):求权值 x 的后继节点

思路同 pre(x),这里不再多解释。

long long suf(long long x){
	find(x);
	if(w[rt]>x) return rt;
	long long p=c[rt][1];
	while(c[p][0]) p=c[p][0];
	return p;
}

10.del(x):删除权值为 x 的数

考虑一件事:如果某个节点是叶节点,那直接把它删了,然后 refresh 父节点就可以了。

然后就有了这个操作:

求出 \(pr=\) pre(x)\(su=\) suf(x)。然后 splay(pr,0),splay(su,pr)

上面操作干了什么事?

先把 \(pr\) 转到根,再把 \(su\) 转到 \(pr\) 的儿子(此时一定为右儿子)。

现在我说:\(su\) 的左儿子一定为叶节点,而且这个左儿子就是我们要找的 \(x\)

因为树中只有这个点恰介于 \(pr\)\(su\) 之间,故 \(su\) 的左儿子一定为 \(x\)

如果 \(x\) 有左儿子,则 \(su\) 应该在 \(x\) 左儿子的位置,不会在根;同理,如果 \(x\) 有右儿子,则 \(su\) 会在 \(x\) 的右儿子。所以 \(x\) 一定为叶节点。

搞完这些,我们只需要看看 \(cnt_x\) 的大小。如果 \(cnt_x>1\),则只需要使 \(cnt_x\) 减 1;否则,直接把它删了就好了。

void del(long long x){
	long long pr=pre(x),su=suf(x);
	splay(pr,0);splay(su,pr);
	long long p=c[su][0];
	if(--cnt[p]){
		splay(p,0);
	}
	else{
		c[su][0]=0;
	}
	refresh(su);refresh(pr);
}

但是,考虑一件事情。如果树中只有一个节点,您爆了。

所以为防止这种情况出现,我们在一开始一般会插入无穷小和无穷大的两个节点。初始化如下:(里面还包含了一些其它的初始化)

void init(){
	long long gx;
	tot=0;
	gx=add(-2147483647);
	gx=add(2147483647);
	rt=1;s[1]=2;c[1][1]=2;f[2]=1;
}

11.kth(x):查询排名为第 x 的数

简单二分思想。

记查询节点 \(p\) 从根开始,如果自己的个数加上左子树的个数比 \(x\) 小,那么往右走;否则如果左子树的个数比 \(x\) 小,则答案就是 \(p\);否则往左跑。

long long kth(long long k){
	long long p=rt;
	while(1){
		if(s[c[p][0]]+cnt[p]<k){
			k-=(s[c[p][0]]+cnt[p]);
			p=c[p][1];
		}
		else{
			if(s[c[p][0]]<k) return p;
			else p=c[p][0];
		}
	}
}

12.rnk(x):查询权值 x 的排名。

求排名,那直接求出有多少个数比它小不就是了。把 \(x\) 旋到根,输出其左子树大小。由于我们之前插入过一个负无穷,所以我们输出的时候就不加 1 了。

long long rnk(long long x){
	find(x);
	return s[c[rt][0]];
}

应注意,这个做法需要保证权值 \(x\) 在树中存在。否则,我们应该先把 \(x\) 插进去,查询 rnk(x),然后再把它删了。比如在P6136 【模板】普通平衡树(数据加强版)中。


于是我们就学会了 Splay 的所有操作了,你已经完全理解了吧!

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

long long m,tot,rt,c[100100][2],f[100010],s[100100],cnt[100010],w[100010];

long long chk(long long x){
	return c[f[x]][1]==x;
}
void refresh(long long x){
	s[x]=s[c[x][0]]+s[c[x][1]]+cnt[x];
}
void rotate(long long x){
	long long y=f[x],z=f[y],k=chk(x);
	c[y][k]=c[x][k^1],f[c[x][k^1]]=y;
	c[z][chk(y)]=x,f[x]=z;
	c[x][k^1]=y,f[y]=x;
	refresh(y),refresh(x);
} 
void splay(long long x,long long goal){
	if(!goal) rt=x;
	long long y,z;
	while(f[x]!=goal){
		y=f[x],z=f[y];
		if(z!=goal) chk(x)^chk(y)?rotate(x):rotate(y);
		rotate(x);
	}
}
void find(long long x){
	if(!rt) return;
	long long p=rt;
	while(w[p]!=x && c[p][x>w[p]]) 
		p=c[p][x>w[p]];
	splay(p,0);
}
long long add(long long x){
	w[++tot]=x;
	cnt[tot]=s[tot]=1;
	f[tot]=c[tot][0]=c[tot][1]=0;
	return tot;
}
void insert(long long x){
	long long p=rt,fp=f[p];
	while(p && w[p]!=x){
		fp=p;
		p=c[p][x>w[p]];
	}
	if(!p){
		p=add(x);
		if(fp)
			c[fp][x>w[fp]]=p;
		f[p]=fp;
	}
	else cnt[p]++;
	splay(p,0);
}
long long pre(long long x){
	find(x);
	if(w[rt]<x) return rt;
	long long p=c[rt][0];
	while(c[p][1]) p=c[p][1];
	return p;
}
long long suf(long long x){
	find(x);
	if(w[rt]>x) return rt;
	long long p=c[rt][1];
	while(c[p][0]) p=c[p][0];
	return p;
}
void del(long long x){
	long long pr=pre(x),su=suf(x);
	splay(pr,0);splay(su,pr);
	long long p=c[su][0];
	if(--cnt[p]){
		splay(p,0);
	}
	else{
		c[su][0]=0;
	}
	refresh(su);refresh(pr);
}
long long rnk(long long x){
	find(x);
	return s[c[rt][0]];
}
long long kth(long long k){
	long long p=rt;
	while(1){
		if(s[c[p][0]]+cnt[p]<k){
			k-=(s[c[p][0]]+cnt[p]);
			p=c[p][1];
		}
		else{
			if(s[c[p][0]]<k) return p;
			else p=c[p][0];
		}
	}
}
void init(){
	long long gx;
	tot=0;
	gx=add(-2147483647);
	gx=add(2147483647);
	rt=1;s[1]=2;c[1][1]=2;f[2]=1;
}

int main(){
	long long i,j,u,v;
	cin>>m;
	init();
	while(m--){
		cin>>j>>u;
		if(j==1){
			insert(u);
		}
 		if(j==2){
			del(u);
		}
		if(j==3){
			cout<<rnk(u)<<endl;
		}
		if(j==4){
			cout<<w[kth(u+1)]<<endl;
		}
		if(j==5){
			cout<<w[pre(u)]<<endl;
		}
		if(j==6){
			cout<<w[suf(u)]<<endl;
		}
	}
	return 0;
}

https://www.luogu.com.cn/paste/tc21xjf7

image

posted @ 2022-05-07 18:08  Gokix  阅读(65)  评论(0编辑  收藏  举报