「学习笔记」Splay

一.什么是 Splay

\(\text{Splay}\) 是一种二叉查找树,它通过不断将某个节点旋转到根节点,使得整棵树仍然满足二叉查找树的性质,并且保持平衡,不退化为链。

二.Splay 的结构

  • \(1.\) 二叉查找树的性质

    首先是一颗二叉树。
    并且左子树任意节点的值 \(<\) 根节点的值 \(<\) 右子树任意节点的值。

  • \(2.\) 节点维护的信息

    \(\text{rt}:\) 根节点编号;
    \(\text{tot}:\) 节点个数;
    \(\text{fa[i]}:\) 父亲;
    \(\text{ch[i][0/1]}:\) 左右儿子编号;
    \(\text{val[i]}:\) 节点权值;
    \(\text{cnt[i]}:\) 权值出现次数;
    \(\text{siz[i]}:\) 子树大小。

三.Splay 的操作

  • 1. pushup

    在改变节点位置后,将节点 \(u\) 的子树大小 \(\text{siz}\) 值更新:

    void pushup (int u) {
    	siz[u] = siz[ch[u][0]] + siz[ch[u][1]] + cnt[u];
    }
    
  • 2. get

    判断节点 \(u\) 是父亲节点的左儿子还是右儿子:
    bool get (int u) {return u == ch[u][1];}

  • 3. clear

    销毁节点 \(u\)

    void clear (int u) {
    	ch[u][0] = ch[u][1] = fa[u] = val[u] = siz[u] = cnt[u] = 0;
    }
    
  • 4. rotate

    将节点 \(u\) 左旋转或者右旋转向上一层,这是为了保证 \(\text{Splay}\) 的平衡。
    可以看做左旋为顺时针方向,右旋为逆时针方向。

    旋转需要保证以下三条要求:

    • 整棵 \(\text{Splay}\) 的中序遍历不变(需要满足二叉查找树的性质)
    • 受影响的节点维护的信息依然正确有效
    • \(\text{root}\) 必须指向旋转后的根节点

    如下图所示,对于 \(2\) 节点进行旋转操作:
    image

    分析一下旋转操作:
    首先设需要旋转的节点为 \(x\),其父亲节点为 \(y\),以右旋为例:

    • \(y\) 的左儿子指向 \(x\) 的右儿子,且 \(x\) 的右儿子的父亲指向 \(y\)
      ch[y][0] = ch[x][1]; fa[ch[x][1]] = y;
    • \(x\) 的右儿子指向 \(y\),且 \(y\) 的父亲指向 \(x\)
      ch[x][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;
    	}
    	pushup (y);
    	pushup (x);
    }
    

    由于左旋和右旋是两种相反的操作,所以我们可以融合到一个函数中,也就出现了 \(\text{xor}1\) 的操作。

  • 5. splay

    \(\text{splay}\) 操作规定:每次访问一个点都要将其旋转至根节点。
    也就是说 \(\text{splay}\) 操作就是将一个点旋转到根节点上。
    接下来对于 \(\text{splay(x)}\)\(6\) 种情况进行讨论:

    • 如果 \(x\) 的父亲是根节点,直接将 \(x\) 左旋或右旋即可。(图 \(1,2\)
      image

    • 如果 \(x\) 的父亲不是根节点,且 \(x\)\(x\) 的父亲的父亲的儿子类型相同,首先将 \(x\) 的父亲的父亲左旋或右旋,然后将 \(x\) 右旋或左旋。(图 \(3,4\))
      image

    • 如果 \(x\) 的父亲不是根节点,且 \(x\)\(x\) 的父亲的父亲的儿子类型不同,将 \(x\) 左旋再右旋、或者右旋再左旋。(图 \(5,6\)
      image

    经过这 \(6\) 种情况模拟我们得知:

    \(x\) 的父亲的父亲存在时:

    • \(x\)\(x\) 的父亲和 \(x\) 的父亲的父亲三个点在同一直线上时,旋转 \(x\) 的父亲;
    • 否则旋转 \(x\) 点。

    代码如下:

    void splay (int x, int aim) {//将 x 旋转到 aim 的儿子处
    	for (int f = fa[x]; f = fa[x], f != aim; rotate (x)) {//不断旋转
    		if (fa[f] != aim) {
    			rotate (get (x) == get (f) ? f : x);
    		}
    	}
    	if (!aim) {
    		root = x;//修改根节点
    	}
    }
    
  • 6. insert

    设插入的值为 \(k\),分三种情况:

    • 如果树为空,则直接插入根并退出。
    • 如果当前节点的权值等于 \(k\),则增加当前节点的大小,并更新节点与父亲的信息,将当前节点进行 \(\text{splay}\) 操作。
    • 否则按二叉查找树的性质向下寻找,找到空节点插入,再进行 \(\text{splay}\) 操作。
    void insert (int k) {
    	if (!root) {//树为空
    		val[++ tot] = k;
    		cnt[tot] ++;
    		root = tot;
    		pushup (root);
    		//新建节点
    		return ;
    	}
    	int now = root, f = 0;
    	while (1) {
    		if (val[now] == k) {//找到权值相同的节点
    			cnt[now] ++;//增加大小
    			pushup (now);
    			pushup (f);
    			splay (now, 0);//旋转到根
    			break;
    		}
    		f = now;//向下搜索
    		now = ch[now][val[now] < k];
    		//当前点权值小于k,则在右儿子处
    		//当前点权值大于k,则在左儿子处
    		if (!now) {//找到空节点
    			val[++ tot] = k;
    			cnt[tot] ++;
    			fa[tot] = f;
    			ch[f][val[f] < k] = tot;//父亲节点的子节点更新
    			pushup (tot);
    			pushup (f);
    			splay (tot, 0);//旋转到根
    			//新建节点
    			break;
    		}
    	}
    }
    
  • 7. get_rank

    查询 \(x\) 值的排名。
    根据二叉查找树的定义和性质,可以按照以下步骤查询 \(x\) 的排名。

    • 如果 \(x\) 比当前点的权值小,向它的左子树搜索。
    • 如果 \(x\) 比当前点的权值大,将答案加上左子树的大小和当前节点的大小,然后向右子树搜索。
    • 如果 \(x\) 与当前点的权值相同,返回当前的答案 \(+1\)

    代码如下:

    int get_rank (int k) {
    	int res = 0, now = root;
    	while (1) {
    		if (k < val[now]) {
    			now = ch[now][0];//向左子树搜索
    		}
    		else {
    			res += siz[ch[now][0]];//增加排名
    			if (k == val[now]) {
    				splay (now, 0);//不要忘记这一步
    				return res + 1;
    			}
    			res += cnt[now];//增加排名
    			now = ch[now][1];//向右子树搜索
    		}
    	}
    }
    
  • 8. get_kth

    查询排名为 \(k\) 的权值。
    分以下两种情况:

    • 如果左子树非空,且剩余排名 \(k\) 不大于左子树的大小 \(siz\),向左子树搜索。
    • 否则将 \(k\) 减去左子树和根的大小。如果此时 \(k\) 的值小于等于 \(0\),那么返回根节点的值。否则继续向右子树搜索。

    代码如下:

    int get_kth (int k) {
    	int now = root;
    	while (1) {
    		if (ch[now][0] && k <= siz[ch[now][0]]) {
    			now = ch[now][0];//向左子树搜索
    		}
    		else {
    			k -= cnt[now];//减去当前节点大小
    			k -= siz[ch[now][0]];//减去左子树大小
    			if (k <= 0) {
    				splay (now, 0);//不要忘记了!
    				return val[now];
    			}
    			now = ch[now][1];//向右子树搜索
    		}
    	}
    }
    
  • 9. get_pre

    查询点 \(x\) 的前驱,即最大的小于 \(x\) 的数。
    那么可以将 \(x\) 插入 \(\text{splay}\)(此时 \(x\) 在根节点),前驱也就是 \(x\) 的左子树中最右的节点,最后将 \(x\) 删除即可。
    代码如下:

    int get_pre () {
    	int now = ch[root][0];
    	if (!now) {
    		return now;
    	}
    	while (ch[now][1]) {
    		now = ch[now][1];//在左子树中寻找最右的节点。
    	}
    	splay (now, 0);//不要忘记!
    	return now;
    }
    
  • 10. get_suf

    查询点 \(x\) 的后继,也就是最小的大于 \(x\) 的数。
    查询方法与前驱相似,查找 \(x\) 的右子树中最左的节点。
    代码如下:

    int get_suf () {
    	int now = ch[root][1];
    	if (!now) {
    		return now;
    	}
    	while (ch[now][0]) {
    		now = ch[now][0];
    	}
    	splay (now, 0);
    	return now;
    }
    
  • 11. merge

    合并两棵 \(\text{Splay}\)。设两棵树的根节点分别为 \(x\)\(y\),那要求 \(x\) 中的最大值小于 \(y\) 中的最小值。合并操作如下:

    • 如果 \(x\)\(y\) 中有一棵空树或都是空树,那么返回非空树或空树。
    • 否则将 \(x\) 中的最大值 \(\text{splay}\) 到根节点,将它的右子树设置为 \(y\) 并更新节点的信息,然后返回这个节点。
  • 12. delete

    删除点 \(x\)

    首先将 \(x\) 旋转到 \(x\) 的位置上:

    • 如果 \(cnt_x>1\),那么将 \(cnt_x-1\) 后退出即可;
    • 否则合并它的两棵子树即可。

    代码如下:

    void del (int k) {
    	get_rank (k);//将k旋转到根
    	if (cnt[root] > 1) {
    		cnt[root] --;//直接减
    		pushup (root);
    		return ;
    	}
    	if (!ch[root][0] && !ch[root][1]) {
    		clear (root);//没有儿子,直接销毁
    		root = 0;
    		return ;
    	}
    	if (!ch[root][0]) {//只有右子树
    		int now = root;
    		root = ch[root][1];
    		fa[root] = 0;
    		clear (now);
    		return ;
    	}
    	if (!ch[root][1]) {//只有左子树
    		int now = root;
    		root = ch[root][0];
    		fa[root] = 0;
    		clear (now);
    		return ;
    	}
    	int now = root, x = get_pre ();
    	fa[ch[now][1]] = x;
    	//根变为原来的根的前驱,
    	//那么原来的右儿子的父亲就变为原来的根的前驱
    	ch[x][1] = ch[now][1];//前驱的右儿子变为原来根的右儿子
    	clear (now);//销毁原来的根
    	pushup (root);
    }
    

四.例题讲解

P3369 【模板】普通平衡树

模板题。将上文讲到的操作融合即可。

代码如下:

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int N = 1e5 + 7;

int rt[N], tot = 0, fa[N], ch[N][2], val[N], cnt[N], siz[N], root;

struct Splay {
	inline void pushup (int u) {
		siz[u] = siz[ch[u][0]] + siz[ch[u][1]] + cnt[u];
	}
	
	inline bool get (int u) {
		return u == ch[fa[u]][1];
	}
	
	inline void clear (int u) {
		ch[u][0] = ch[u][1] = fa[u] = val[u] = siz[u] = cnt[u] = 0;
	}
	
	inline 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;
		}
		pushup (y);
		pushup (x);
	}
	
	inline void splay (int x, int aim = 0) {
		for (int f = fa[x]; f = fa[x], f != aim; rotate (x)) {
			if (fa[f] != aim) {
				rotate (get (x) == get (f) ? f : x);
			}
		} 
		
		if (!aim) {
			root = x;
		}
	}
	
	inline void insert (int k) {
		if (!root) {
			val[++ tot] = k;
			cnt[tot] ++;
			root = tot;
			pushup (root);
			
			return ;
		}
		int now = root, f = 0;
		while (1) {
			if (val[now] == k) {
				cnt[now] ++;
				pushup (now);
				pushup (f);
				splay (now, 0);
				
				break;
			}
			
			f = now;
			now = ch[now][val[now] < k];
			
			if (!now) {
				val[++ tot] = k;
				cnt[tot] ++;
				fa[tot] = f;
				ch[f][val[f] < k] = tot;
				pushup (tot);
				pushup (f);
				splay (tot, 0);
				
				break;
			}
		}
	}
	
	int get_rank (int k) {
		int res = 0, now = root;
		while (1) {
			if (k < val[now]) {
				now = ch[now][0];
			}
			else {
				res += siz[ch[now][0]];
				if (k == val[now]) {
					splay (now, 0);
					return res + 1;
				}
				res += cnt[now];
				now = ch[now][1];
			}
		}
	}
	
	int get_kth (int k) {
		int now = root;
		while (1) {
			if (ch[now][0] && k <= siz[ch[now][0]]) {
				now = ch[now][0];
			}
			
			else {
				k -= cnt[now];
				k -= siz[ch[now][0]];
				if (k <= 0) {
					splay (now, 0);
					return val[now];
				}
				now = ch[now][1];
			}
		}
	}
	
	int get_pre () {
		int now = ch[root][0];
		if (!now) {
			return now;
		}
		while (ch[now][1]) {
			now = ch[now][1];
		}
		splay (now, 0);
		return now;
	}
	
	int get_suf () {
		int now = ch[root][1];
		if (!now) {
			return now;
		}
		while (ch[now][0]) {
			now = ch[now][0];
		}
		splay (now, 0);
		return now;
	}
	
	void del (int k) {
		get_rank (k);
		if (cnt[root] > 1) {
			cnt[root] --;
			pushup (root);
			return ;
		}
		if (!ch[root][0] && !ch[root][1]) {
			clear (root);
			root = 0;
			return ;
		}
		if (!ch[root][0]) {
			int now = root;
			root = ch[root][1];
			fa[root] = 0;
			clear (now);
			return ;
		}
		if (!ch[root][1]) {
			int now = root;
			root = ch[root][0];
			fa[root] = 0;
			clear (now);
			return ;
		}
		int now = root, x = get_pre ();
		fa[ch[now][1]] = x;
		ch[x][1] = ch[now][1];
		clear (now);
		pushup (root);
 	}
}tree;

int n, op, x;

int main () {
	scanf ("%d", &n);
	
	for (int i = 1; i <= n; i ++) {
		scanf ("%d%d", &op, &x);
		
		if (op == 1) {
			tree.insert (x);
		}
		
		if (op == 2) {
			tree.del (x);
		}
		
		if (op == 3) {
			printf ("%d\n", tree.get_rank (x));
		}
		
		if (op == 4) {
			printf ("%d\n", tree.get_kth (x));
		}
		
		if (op == 5) {
			tree.insert (x);
			printf ("%d\n", val[tree.get_pre ()]);
			tree.del (x);
		}
		
		if (op == 6) {
			tree.insert (x);
			printf ("%d\n", val[tree.get_suf ()]);
			tree.del (x);
		}
	}
	
	return 0;
}
posted @ 2022-02-09 09:56  cyhyyds  阅读(152)  评论(0编辑  收藏  举报