【Coel.算法笔记】【隐忍与偏见】伸展平衡树(Splay)-基本操作

题前碎语

还有大概一个月的时间就可以离开这个班了,继续坚持……
尽管得忍受无数的冷眼、无数的唾弃与压力……
嘛,坚持下去总会成的,不是吗?

题目简介

P3369 【模板】普通平衡树
洛谷传送门
题目和FHQ_Treap的一样,这里就不放了。

正文

平衡树有很多,这里再介绍一个——伸展树,即\(Splay\)
什么,你问我为什么叫\(Splay\)
自己去百度翻译一下。

基本思路

回归正题,\(Splay\)的原理是每次访问节点都把它伸展到根节点,从而保持平衡。
正是因为这一特点,\(Splay\)能够实现区间分裂、翻转等高级操作,并且是动态树\(Link-Cut-Tree\)的基础,这是\(Treap\)所不能比拟的。
FHQ-Treap:对不起,我也可以
但正如学长\(Sherlockk\)所说,二者的关系就跟树状数组与线段树一样,各取所需,各有所长,所以都得学学。
那么,正式开始吧!

操作合集

初始化

按照之前的套路,我们把所有数据存到一个结构体里面。

int root, tot;

struct Splay_Tree {
	int fa[maxn], size[maxn], ch[maxn][2], cnt[maxn], val[maxn];
}S;

root为根节点,tot为节点编号 ,其它顾名思义

前置操作:更新、判断与删除

更新也就是\(pushup\),把\(size\)数组更新。

inline void pushup(int x) {
    size[x] = size[ch[x][0]] + size[ch[x][1]] + cnt[x];
}

判断\(get-which\),识别当前节点是左还是右儿子,这在伸展和旋转的时候需要使用,可以减少代码量。

inline bool get_which(int x) {
    return ch[fa[x]][1] == x;
}

删除\(delete\),把当前节点清空。

inline void clear(int x) {
    ch[x][0] = ch[x][1] = cnt[x] = size[x] = val[x] = fa[x] = 0; 
}

rotate旋转

虽然我个人比较喜欢把它叫做\(spin\),不过为了统一还是写\(rotate\)
引用一下皎月半洒花的一句话:

我们定义一个结点与他父亲的关系是x,那么在旋转时,他的父亲成为了他的!x儿子(!x为x之外另一个儿子,例如x为左,那么!x就是右),并且“多余结点”,同时也是当前节点的!x儿子,在旋转之后需要成为当前节点的“前”父节点的x儿子

这条规律是旋转的核心,有了它,我们就可以完美实现。
为了方便,我们用右旋为例子讲解。
记原本节点为x,它的父节点为f,f的父节点为f_f,则:
1.f左儿子指向x右儿子,x的右儿子的父节点指向f;
2.f的右儿子指向f,f的父节点指向x;
3.如果f_f存在,那么把它剩下的儿子指向x,x的父节点指向f_f。
说起来有点绕,不过也没办法\(qwq\)
还记得之前的\(get-which\)吗?有了它我们可以把左旋和右旋合在一起写,方便很多。
顺便提一下异或,对于一个非0即1的数字,对其异或1可以把它转换,也就是1^1=0,0^1=1,这样就能简洁表示左右儿子。

inline void rotate(int x) {
    int f = fa[x], f_f = fa[f], d = get_which(x);
    ch[f][d] = ch[x][d ^ 1];
    fa[ch[f][d]] = f;
    ch[x][d ^ 1] = f;
    fa[f] = x;
    fa[x] = f_f;
    if (f_f)
        ch[f_f][f == ch[f_f][1]] = x;
    pushup(x), pushup(f);//别忘了更新
}

Splay伸展

是的,\(Splay\)就是伸展的英文。
伸展听起来很厉害,不过只是某个节点旋转到根节点罢了\(awa\)
分为6种情况,但其实可以合并成3种~
1.x的父亲是根节点,直接进行一次旋转。
2.如果不是根节点,并且x,x的父亲,x的祖父三点共线(都是左/右子树),那么先转父亲,再转儿子
3.如果三点不共线,那么转儿子两次。
最后,把根节点更新。
以下代码非常简洁,建议仔细研究。

inline void splay(int x) {
	for (int f; (f = fa[x]) != 0; rotate(x))
   		if (fa[f]) rotate(get_which(f) == get_which(x) ? f : x);
	root = x;
}

insert插入

根据二叉搜索树的性质插入。别忘了你在写二叉搜索树
1.当前树是空的,直接插入。
2.当前节点的值可要插入的值相同,更新当前节点和父节点信息,并进行一次\(Splay\)
3.否则继续向下找,找到就插入,然后\(Splay\)

void insert(int x) {
	if (root == 0) {//对应可能1
		val[++tot] = x;
		cnt[tot]++;
			root = tot;
		pushup(root);
		return;
	}
	int now = root, f = 0;
	while (1) {
		if (val[now] == x) {
			cnt[now]++;
			pushup(now), pushup(f);
			splay(now);
			return;
		}//对应可能2
		else {//对应可能3
			f = now;
			now = ch[now][val[now] < x];
			if (now == 0) {//向下找并对应可能2
				val[++tot] = x;
				cnt[tot]++;
				fa[tot] = f;
				ch[f][val[f] < x] = tot;//判断左右子树	
                pushup(tot), pushup(f);
				splay(tot);
				return;
			}
		}
	}
}

Query_rank已知值查排名

1.当前节点比值小,往左边找;
2.当前节点比值大,往右边找,答案加上左边大小;
3.找到了,把答案+1,进行\(Splay\),结束。

int Query_rank(int x) {
	int res = 0, now = root;
	while (1) {
		if (x < val[now])
			now = ch[now][0];
		else {
			res += size[ch[now][0]];
			if (x == val[now]) {
				splay(now);
				return res + 1;
			}
			res += cnt[now];
			now = ch[now][1];
		}
	}
}

Query_num已知排名查值

1.左子树非空,排名比左子树大小要小,往左边找;
2.否则把排名减去左子树大小和左节点大小,小于0就得到答案,\(Splay\)后结束;反之往右边找。

int Query_num(int k) {
	int now = root;
	while (1) {
		if (ch[now][0] && k <= size[ch[now][0]])
			now = ch[now][0];
		else {
			k -= cnt[now] + size[ch[now][0]];
			if (k <= 0) {
				splay(now);
				return val[now];
			}
			else now = ch[now][1];
		}
	}
}

查前驱pre与查后继pos

采用一种很巧妙的方式:插入数值,前驱为左子树最右边节点,后继为右子树最左边节点,找到之后把数值删掉,别忘了\(Splay\)
主程序内容:

S.insert(x);
write(S.val[S.Query_pre()]);//后继对应pos
S.erase(x);

前驱与后继对应代码:

int Query_pre() {
	int now = ch[root][0];
	if (now == 0) return now;
	while (ch[now][1])
		now = ch[now][1];
	splay(now);
	return now;
}
	
int Query_pos() {
	int now = ch[root][1];
	if (now == 0) return now;
	while (ch[now][0])
		now = ch[now][0];
	splay(now);
	return now;
}

erase删除

1.把数值旋转到根,这里利用查排名的操作旋转。
2.如果对应数值不止一个,把cnt[x]减去1;
3.否则把左右子树合并。

合并操作

1.如果两个子树有一个是空的,把另一边返回;
2.否则,否则\(Splay\)左树中的最大值,然后把它的右子树设置为原来的右子树,返回节点。
啊啊啊好复杂

void erase(int x) {
	Query_rank(x);
	if (cnt[root] > 1) {//对应可能2
		cnt[root]--;
		pushup(root);
		return;
	}
	if (!ch[root][1] && !ch[root][0]) {//左右都是空,直接清除
		clear(root);
		root = 0;
		return;
	}
	if (!ch[root][0]) {//左边空
		int now = root;
		root = ch[root][1];
		fa[root] = 0;
		clear(now);
		return;
	}
	else if(!ch[root][1]) {//右边空
		int now = root;
		root = ch[root][0];
		fa[root] = 0;
		clear(now);
		return;
	}
    //左右都不空
	int now = root, pre = Query_pre();
	fa[ch[now][1]] = pre;
	ch[pre][1] = ch[now][1];
	clear(now);
	pushup(root);
}

完整代码

终于要结束了……

#include <iostream>
#include <cstdio>
#include <cctype>

namespace FastIO {
	inline int read() {
		int x = 0, f = 1;
		char ch = getchar();
		while (!isdigit(ch)) {
			if (ch == '-') f = -1;
			ch = getchar();
		}
		while (isdigit(ch)) {
			x = x * 10 + ch - '0';
			ch = getchar();
		}
		return x * f;
	}
	inline void write(int x) {
		if (x < 0) {
			x = -x;
			putchar('-');
		}
		static int buf[35];
		int top = 0;
		do {
			buf[top++] = x % 10;
			x /= 10;
		} while (x);
		while (top)
			putchar(buf[--top] + '0');
		puts("");
	}
}

using namespace std;
using namespace FastIO;

const int maxn = 1e5 + 10;

int root, tot;

struct Splay_Tree {
	int fa[maxn], size[maxn], ch[maxn][2], cnt[maxn], val[maxn];
	
	inline void pushup(int x) {
		size[x] = size[ch[x][0]] + size[ch[x][1]] + cnt[x];
	}
	
	inline bool get_which(int x) {
		return ch[fa[x]][1] == x;
	}
	
	inline void clear(int x) {
		ch[x][0] = ch[x][1] = cnt[x] = size[x] = val[x] = fa[x] = 0; 
	}
	
	inline void rotate(int x) {
		int f = fa[x], f_f = fa[f], d = get_which(x);
		ch[f][d] = ch[x][d ^ 1];
		fa[ch[f][d]] = f;
		ch[x][d ^ 1] = f;
		fa[f] = x;
		fa[x] = f_f;
		if (f_f)
			ch[f_f][f == ch[f_f][1]] = x;
		pushup(x), pushup(f);
	}
	
	inline void splay(int x) {
		for (int f; (f = fa[x]) != 0; rotate(x))
			if (fa[f]) rotate(get_which(f) == get_which(x) ? f : x);
		root = x;
	}
	
	void insert(int x) {
		if (root == 0) {
			val[++tot] = x;
			cnt[tot]++;
			root = tot;
			pushup(root);
			return;
		}
		int now = root, f = 0;
		while (1) {
			if (val[now] == x) {
				cnt[now]++;
				pushup(now), pushup(f);
				splay(now);
				return;
			}
			else {
				f = now;
				now = ch[now][val[now] < x];
				if (now == 0) {
					val[++tot] = x;
					cnt[tot]++;
					fa[tot] = f;
					ch[f][val[f] < x] = tot;
					pushup(tot), pushup(f);
					splay(tot);
					return;
				}
			}
		}
	}
	
	int Query_num(int k) {
		int now = root;
		while (1) {
			if (ch[now][0] && k <= size[ch[now][0]])
				now = ch[now][0];
			else {
				k -= cnt[now] + size[ch[now][0]];
				if (k <= 0) {
					splay(now);
					return val[now];
				}
				else now = ch[now][1];
			}
		}
	}
	
	int Query_rank(int x) {
		int res = 0, now = root;
		while (1) {
			if (x < val[now])
				now = ch[now][0];
			else {
				res += size[ch[now][0]];
				if (x == val[now]) {
					splay(now);
					return res + 1;
				}
				res += cnt[now];
				now = ch[now][1];
			}
		}
	}
	
	int Query_pre() {
		int now = ch[root][0];
		if (now == 0) return now;
		while (ch[now][1])
			now = ch[now][1];
		splay(now);
		return now;
	}
	
	int Query_pos() {
		int now = ch[root][1];
		if (now == 0) return now;
		while (ch[now][0])
			now = ch[now][0];
		splay(now);
		return now;
	}
	
	void erase(int x) {
		Query_rank(x);
		if (cnt[root] > 1) {
			cnt[root]--;
			pushup(root);
			return;
		}
		if (!ch[root][1] && !ch[root][0]) {
			clear(root);
			root = 0;
			return;
		}
		if (!ch[root][0]) {
			int now = root;
			root = ch[root][1];
			fa[root] = 0;
			clear(now);
			return;
		}
		else if(!ch[root][1]) {
			int now = root;
			root = ch[root][0];
			fa[root] = 0;
			clear(now);
			return;
		}
		int now = root, pre = Query_pre();
		fa[ch[now][1]] = pre;
		ch[pre][1] = ch[now][1];
		clear(now);
		pushup(root);
	}
	
}S;

int main(void) {
	int n = read();
	for (int i = 1; i <= n; i++) {
		int opt = read(), x = read();
		if (opt == 1)
			S.insert(x);
		else if (opt == 2)
			S.erase(x);
		else if (opt == 3)
			write(S.Query_rank(x));
		else if (opt == 4)
			write(S.Query_num(x));
		else if (opt == 5) {
			S.insert(x);
			write(S.val[S.Query_pre()]);
			S.erase(x);
		}
		else if (opt == 6) {
			S.insert(x);
			write(S.val[S.Query_pos()]);
			S.erase(x);
		}
	}
	return 0;
}

题后闲话

这是我写博客用时最久的一次,也说明了\(Splay\)真的很重要\(qwq\)
下次把进阶操作补上吧,今天好累\(quq\)
(后记:不想补了!咕咕咕)

posted @ 2022-03-24 19:42  秋泉こあい  阅读(76)  评论(0编辑  收藏  举报