Splay 学习笔记

Splay 树, 或 伸展树,是一种平衡二叉查找树,它通过 Splay/伸展操作 不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,能够在均摊 O(\log N) 时间内完成插入,查找和删除操作,并且保持平衡而不至于退化为链。

Splay 树由 Daniel Sleator 和 Robert Tarjan 于 1985 年发明。
code:

using namespace std;
const int maxn = 1e5+10;
int root,tot,fa[maxn],ch[maxn][2],val[maxn],cnt[maxn],sz[maxn];
int rt;
struct Splay{
	void maintain(int x){
		sz[x] = sz[ch[x][0]]+sz[ch[x][1]]+cnt[x];//在改变节点位置后,将节点x的size更新 
	}
	bool get(int x){//判断x是父节点的左儿子还是右儿子 
		return x == ch[fa[x]][1];
	}
	void clear(int x){//销毁节点x 
		ch[x][0] = ch[x][1] = fa[x] = val[x] = sz[x] = cnt[x] = 0;
	}
	/*
	将 y 的左儿子指向 x 的右儿子,且 x 的右儿子(如果 x 有右儿子的话)
	的父亲指向 y;ch[y][0]=ch[x][1]; fa[ch[x][1]]=y;
	将 x 的右儿子指向 y,且 y 的父亲指向 x;ch[x][chk^1]=y; fa[y]=x;
    若原来的 y 还有父亲 z,那么把 z 的某个儿子(原来 y 所在的儿子位置)
	指向 x,且 x 的父亲指向 z。fa[x]=z; if(z) ch[z][y==ch[z][1]]=x;
	*/ 
	void rotate(int x){
		int y = fa[x],z = fa[y],chk = get(x);
		ch[y][chk] = ch[x][chk^1];
		if(ch[x][chk^1]) fa[ch[x][chk^1]] = y;
		ch[x][chk^1] = y;
		fa[y] = x;
		fa[x] = z;
		if(z) ch[z][y == ch[z][1]] = x;
		maintain(y);
		maintain(x); 
	}
	/*
	Splay 操作规定:每访问一个节点 x 后都要强制将其旋转到根节点。
	Splay 操作即对 x 做一系列的 splay 步骤。每次对 x 做一次 splay 步
	骤,x 到根节点的距离都会更近。定义 p 为 x 的父节点。Splay 步骤有
	三种,具体分为六种情况:
	*/
	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;
	}
	/*
	如果树空了,则直接插入根并退出。
	如果当前节点的权值等于 k 则增加当前节点的大小
	并更新节点和父亲的信息,将当前节点进行 Splay 操作。
	否则按照二叉查找树的性质向下找,找到空节点就插入即
	可(请不要忘记 Splay 操作)。
	*/
	void ins(int k){
		if(!rt){
			val[++tot] = k;
			cnt[tot]++;
			rt = tot;
			maintain(rt);
			return;
		}
		int cur = rt,f = 0;
		while(1){
			if(val[cur] == k){
				cnt[cur]++;
				maintain(cur);
				maintain(f);
				splay(cur);
				break;
			}
			f = cur;
			cur = ch[cur][val[cur]<k];
			if(!cur){
				val[++tot] = k;
				cnt[tot]++;
				fa[tot] = f;
				ch[f][val[f]<k] = tot;
				maintain(f);
				maintain(tot);
				splay(tot);
				break;
			}
		}
	}
	/*
	如果 x 比当前节点的权值小,向其左子树查找。
	如果 x 比当前节点的权值大,将答案加上左子树(size)和当前节点(cnt)的大小,向其右子树查找。
	如果 x 与当前节点的权值相同,将答案加 1 并返回。
	*/
	int rk(int k){
		int res = 0,cur = rt;
		while(1){
			if(k < val[cur]){
				cur = ch[cur][0];
			}
			else{
				res += sz[ch[cur][0]];
				if(!cur) return res+1;
				if(k == val[cur]){
					splay(cur);
					return res+1;
				}
				res += cnt[cur];
				cur = ch[cur][1];
			}
		}
	}
	/*
	如果左子树非空且剩余排名 k 不大于左子树的大小 size,那么向左子树查找。
	否则将 k 减去左子树的和根的大小。如果此时 k 的值小于等于 0,则返回根节点
	的权值,否则继续向右子树查找。
	*/
	int kth(int k) {
    	int cur = rt;
    	while (1) {
      		if (ch[cur][0] && k <= sz[ch[cur][0]]) {
        	cur = ch[cur][0];
      		} 
			else {
        		k -= cnt[cur] + sz[ch[cur][0]];
        		if (k <= 0) {
        			splay(cur);
        			return val[cur];
        		}
        	cur = ch[cur][1];
      		}
    	}
  	}
  	/*
  	前驱定义为小于 x 的最大的数,那么查询前驱可以转化为:将 x 插入(
	  此时 x 已经在根的位置了),前驱即为 x 的左子树中最右边的节点,最后将 x 删除即可。
  	*/
	int pre(){
		int cur = ch[rt][0];
		if(!cur) return cur;
		while(ch[cur][1]) cur = ch[cur][1];
		splay(cur);
		return cur;
	}
	int nex(){//后继 
		int cur = ch[rt][1];
		if (!cur) return cur;
		while(ch[cur][0]) cur = ch[cur][0];
		splay(cur);
		return cur;
	}
	/*
	首先将 x 旋转到根的位置。

如果 cnt[x]>1(有不止一个 x),那么将 cnt[x] 减 1 并退出。
否则,合并它的左右两棵子树即可。


合并:
如果 x 和 y 其中之一或两者都为空树,直接返回不为空的那一棵树的根节点或空树。
否则将 x 树中的最大值 \operatorname{Splay} 到根,然后把它的右子树设置为 y 并
更新节点的信息,然后返回这个节点。 
*/
	void del(int k) {
    	rk(k);
    	if (cnt[rt] > 1) {
      		cnt[rt]--;
      		maintain(rt);
      		return;
   		}
    if (!ch[rt][0] && !ch[rt][1]) {
      	clear(rt);
      	rt = 0;
      	return;
    }
    if (!ch[rt][0]) {
      	int cur = rt;
      	rt = ch[rt][1];
      	fa[rt] = 0;
      	clear(cur);
      	return;
    }
    if (!ch[rt][1]) {
      	int cur = rt;
      	rt = ch[rt][0];
      	fa[rt] = 0;
      	clear(cur);
      	return;
    }
    int cur = rt;
    int x = pre();
    fa[ch[cur][1]] = x;
    ch[x][1] = ch[cur][1];
    clear(cur);
    maintain(rt);
  	}
	
}tree;
int n;
int main(){
	cin>>n;
	while(n--){
		int opt,x;
		cin>>opt>>x;
		 if (opt == 1)
     	 	tree.ins(x);
		 else if (opt == 2)
         	tree.del(x);
         else if (opt == 3)
         	printf("%d\n", tree.rk(x));
         else if (opt == 4)
         	printf("%d\n", tree.kth(x));
         else if (opt == 5)
         	tree.ins(x), printf("%d\n", val[tree.pre()]), tree.del(x);
         else
        	tree.ins(x), printf("%d\n", val[tree.nex()]), tree.del(x);
	}
	return 0;
}

旋转操作

为了使 Splay 保持平衡而进行旋转操作,旋转的本质是将某个节点上移一个位置。

旋转需要保证:

整棵 Splay 的中序遍历不变(不能破坏二叉查找树的性质)。
受影响的节点维护的信息依然正确有效。
root 必须指向旋转后的根节点。
在 Splay 中旋转分为两种:左旋和右旋。

过程
具体分析旋转步骤(假设需要旋转的节点为 x,其父亲为 y,以右旋为例)

将 y 的左儿子指向 x 的右儿子,且 x 的右儿子(如果 x 有右儿子的话)的父亲指向 y;ch[y][0]=ch[x][1]; fa[ch[x][1]]=y;
image

将 x 的右儿子指向 y,且 y 的父亲指向 x;ch[x][chk^1]=y; fa[y]=x;
如果原来的 y 还有父亲 z,那么把 z 的某个儿子(原来 y 所在的儿子位置)指向 x,且 x 的父亲指向 z。fa[x]=z; if(z) ch[z][y==ch[z][1]]=x;

Splay 操作

Splay 操作规定:每访问一个节点 x 后都要强制将其旋转到根节点。

Splay 操作即对 x 做一系列的 splay 步骤。每次对 x 做一次 splay 步骤,x 到根节点的距离都会更近。定义 p 为 x 的父节点。Splay 步骤有三种,具体分为六种情况:

zig: 在 p 是根节点时操作。Splay 树会根据 x 和 p 间的边旋转。zig 存在是用于处理奇偶校验问题,仅当 x 在 splay 操作开始时具有奇数深度时作为 splay 操作的最后一步执行。

splay-zig

image

即直接将 x 左旋或右旋(图 1, 2)

图 1
image

图 2
image

zig-zig: 在 p 不是根节点且 x 和 p 都是右侧子节点或都是左侧子节点时操作。下方例图显示了 x 和 p 都是左侧子节点时的情况。Splay 树首先按照连接 p 与其父节点 g 边旋转,然后按照连接 x 和 p 的边旋转。

splay-zig-zig

即首先将 g 左旋或右旋,然后将 x 右旋或左旋(图 3, 4)。
图 3
image
图 4
image

zig-zag: 在 p 不是根节点且 x 和 p 一个是右侧子节点一个是左侧子节点时操作。Splay 树首先按 p 和 x 之间的边旋转,然后按 x 和 g 新生成的结果边旋转。

splay-zig-zag

image

即将 x 先左旋再右旋、或先右旋再左旋(图 5, 6)。

图 5
image

图 6
image

posted @ 2024-07-17 20:18  Dreamers_Seve  阅读(16)  评论(1编辑  收藏  举报