Splay 与 Link-Cut Tree 学习笔记(没写完)


Splay 与 Link-Cut Tree 学习笔记

Splay 学习笔记

Splay 简介

个人感觉 Splay 没有 FHQ-Treap 用着舒服,代码也长很多,但是为了学 LCT,不得不学一下 Splay。

Splay(伸展树)是一种二叉查找树,通过不断地将节点旋转到根节点,使得整棵树仍满足二叉查找树的性质,并且能够保持平衡而不退化成链,由 Daniel Sleator 和 Robert Tarjan 发明。

Splay 结构

Splay 首先是一棵二叉查找树。

Splay 需要维护如下信息:

  • \(rt\):根节点。
  • \(sz\):节点个数。

同时,每个节点需要维护如下信息:

  • \(fa\):父亲节点。
  • \({son}_{0/1}\):左右节点。
  • \(val\):权值。
  • \(cnt\):有几个数。
  • \(sz\):子树大小。

Splay 操作

pushup 操作

pushup 用于更新节点信息。

void pushup(int u) {t[u].sz = t[t[u].son[0]].sz + t[t[u].son[1]].sz + t[u].cnt;}

get 操作

get 用于判断一个节点是它的父亲的哪个儿子。

bool get(int u) {return u == t[t[u].fa].son[1];}

rotate 操作

rotate 用于将一个节点上移一个位置,要满足二叉查找树性质。

有两种旋转:左旋和右旋。这里贺一张 OI Wiki 图片过来:

观察左侧图片到右侧图片的“右旋 \(2\)”过程,进行的操作如下:

  • \(2\) 的右儿子变为 \(1\) 的左儿子。
  • \(1\) 变为 \(2\) 的右儿子。
  • 如果 \(1\) 有父亲,将原来 \(1\) 的位置换成 \(2\)

容易发现这棵树的中序遍历不变,也就是说依然满足二叉查找树的性质。为了使每个节点维护的信息依然有效,需要依次对 \(1,2\) 进行 pushup 操作。

左旋同理。

void rotate(int u) {
	int f = t[u].fa, p = t[f].fa, id = get(u), idf = get(f);
	t[f].son[id] = t[u].son[id^1];
	if(t[u].son[id^1]) t[t[u].son[id^1]].fa = f;
	t[u].son[id^1] = f;
	t[f].fa = u;
	t[u].fa = p;
	if(p) t[p].son[idf] = u;
	pushup(f);
	pushup(u);
}

splay 操作

splay 用于将一个节点旋转到根,Splay 规定每访问一个节点都要进行这个操作。

分三种情况考虑:节点的父亲是根、节点和父亲的关系与父亲和祖父的关系相同、节点和父亲的关系与父亲和祖父的关系不同。

下面是这三种情况对应的图,为了理解这一操作,建议自行画图模拟一下(我画了一白板莫名爆字迹):

操作复杂但代码十分简短:

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

insert 操作

insert 用于插入一个数。

分几种情况:

  • 如果树是空的,新建一个节点插进去即可。
  • 否则根据二叉查找树性质找到要插入的位置,如果已经有节点,就把 \(cnt\) 加一然后 pushup,否则新建一个节点插进去。

记得进行 splay 操作。

void insert(int w) {
	int u = rt, f = 0;
	if(!u) {
		u = ++sz;
		t[u].val = w;
		t[u].cnt = 1;
		rt = u;
		pushup(u);
		return;
	}
	while(true) {
		if(t[u].val == w) {
			++t[u].cnt;
			pushup(u);
			pushup(f);
			splay(u);
			break;
		}
		f = u;
		u = t[u].son[t[u].val < w];
		if(!u) {
			u = ++sz;
			t[u].val = w;
			t[u].cnt = 1;
			t[u].fa = f;
			t[f].son[t[f].val < w] = u;
			pushup(u);
			pushup(f);
			splay(u);
			break;
		}
	}
}

rk 操作

rk 用于查询一个数的排名。

根据二叉查找树性质找到对应位置即可,记得记录有多少个数比查询的数小。

int rk(int w) {
	int u = rt, ans = 0;
	while(true) {
		if(w < t[u].val) {
			u = t[u].son[0];
			continue;
		}
		ans += t[t[u].son[0]].sz;
		if(w == t[u].val) {
			splay(u);
			return ans + 1;
		}
		ans += t[u].cnt;
		u = t[u].son[1];
	}
}

kth 操作

kth 用于查询排名为 \(k\) 的数。

根据二叉查找树性质找到对应位置即可。

int kth(int k) {
	int u = rt;
	while(true) {
		if(t[u].son[0] && k <= t[t[u].son[0]].sz) {
			u = t[u].son[0];
			continue;
		}
		k -= t[u].cnt + t[t[u].son[0]].sz;
		if(k <= 0) {
			splay(u);
			return t[u].val;
		}
		u = t[u].son[1];
	}
}

pre 操作和 suc 操作

pre 用于查询根节点的前驱,suc 用于查询根节点的后继。

查询任意数的前驱的做法为先插入,再查根节点前驱,最后删除,后继同理。

根节点的前驱就是左子树内最靠右的节点,后继同理。

int pre() {
	int u = t[rt].son[0];
	if(!u) return 0;
	while(t[u].son[1]) u = t[u].son[1];
	splay(u);
	return u;
}
int suc() {
	int u = t[rt].son[1];
	if(!u) return 0;
	while(t[u].son[0]) u = t[u].son[0];
	splay(u);
	return u;
}

erase 操作

erase 用于删除一个数。

首先将要删的数 splay 到根,然后分几种情况:

  • 如果 \(cnt > 1\),减小一并 pushup 即可。
  • 如果既没有左儿子,又没有右儿子,那么直接删掉即可。
  • 如果有其中一个儿子,将它放到根并删掉当前节点即可。
  • 否则把前驱 splay 到根,并把右子树接到前驱上,然后删掉当前节点即可。
void erase(int w) {
	rk(w);
	if(t[rt].cnt > 1) {
		--t[rt].cnt;
		pushup(rt);
		return;
	}
	if(!t[rt].son[0] && !t[rt].son[1]) {
		t[rt] = Node();
		rt = 0;
		return;
	}
	if(!t[rt].son[0]) {
		int u = rt;
		rt = t[rt].son[1];
		t[rt].fa = 0;
		t[u] = Node();
		return;
	}
	if(!t[rt].son[1]) {
		int u = rt;
		rt = t[rt].son[0];
		t[rt].fa = 0;
		t[u] = Node();
		return;
	}
	int u = rt, x = pre();
	t[t[u].son[1]].fa = x;
	t[x].son[1] = t[u].son[1];
	t[u] = Node();
	pushup(rt);
}

普通平衡树

超长的代码:

// Problem: P3369 【模板】普通平衡树
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3369
// Memory Limit: 128 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

//By: OIer rui_er
#include 
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;
const int N = 2e5+5;

int n;
template void chkmin(T& x, T y) {if(x > y) x = y;}
template void chkmax(T& x, T y) {if(x < y) x = y;}
struct Node {
	int fa, son[2], val, cnt, sz;
	Node() {fa = son[0] = son[1] = val = cnt = sz = 0;}
};
struct Splay {
	Node t[N];
	int rt, sz;
	void pushup(int u) {t[u].sz = t[t[u].son[0]].sz + t[t[u].son[1]].sz + t[u].cnt;}
	bool get(int u) {return u == t[t[u].fa].son[1];}
	void rotate(int u) {
		int f = t[u].fa, p = t[f].fa, id = get(u), idf = get(f);
		t[f].son[id] = t[u].son[id^1];
		if(t[u].son[id^1]) t[t[u].son[id^1]].fa = f;
		t[u].son[id^1] = f;
		t[f].fa = u;
		t[u].fa = p;
		if(p) t[p].son[idf] = u;
		pushup(f);
		pushup(u);
	}
	void splay(int u) {
		for(int f=t[u].fa;f=t[u].fa;rotate(u)) {
			if(t[f].fa) rotate(get(u) == get(f) ? f : u);
		}
		rt = u;
	}
	void insert(int w) {
		int u = rt, f = 0;
		if(!u) {
			u = ++sz;
			t[u].val = w;
			t[u].cnt = 1;
			rt = u;
			pushup(u);
			return;
		}
		while(true) {
			if(t[u].val == w) {
				++t[u].cnt;
				pushup(u);
				pushup(f);
				splay(u);
				break;
			}
			f = u;
			u = t[u].son[t[u].val < w];
			if(!u) {
				u = ++sz;
				t[u].val = w;
				t[u].cnt = 1;
				t[u].fa = f;
				t[f].son[t[f].val < w] = u;
				pushup(u);
				pushup(f);
				splay(u);
				break;
			}
		}
	}
	int rk(int w) {
		int u = rt, ans = 0;
		while(true) {
			if(w < t[u].val) {
				u = t[u].son[0];
				continue;
			}
			ans += t[t[u].son[0]].sz;
			if(w == t[u].val) {
				splay(u);
				return ans + 1;
			}
			ans += t[u].cnt;
			u = t[u].son[1];
		}
	}
	int kth(int k) {
		int u = rt;
		while(true) {
			if(t[u].son[0] && k <= t[t[u].son[0]].sz) {
				u = t[u].son[0];
				continue;
			}
			k -= t[u].cnt + t[t[u].son[0]].sz;
			if(k <= 0) {
				splay(u);
				return t[u].val;
			}
			u = t[u].son[1];
		}
	}
	int pre() {
		int u = t[rt].son[0];
		if(!u) return 0;
		while(t[u].son[1]) u = t[u].son[1];
		splay(u);
		return u;
	}
	int suc() {
		int u = t[rt].son[1];
		if(!u) return 0;
		while(t[u].son[0]) u = t[u].son[0];
		splay(u);
		return u;
	}
	void erase(int w) {
		rk(w);
		if(t[rt].cnt > 1) {
			--t[rt].cnt;
			pushup(rt);
			return;
		}
		if(!t[rt].son[0] && !t[rt].son[1]) {
			t[rt] = Node();
			rt = 0;
			return;
		}
		if(!t[rt].son[0]) {
			int u = rt;
			rt = t[rt].son[1];
			t[rt].fa = 0;
			t[u] = Node();
			return;
		}
		if(!t[rt].son[1]) {
			int u = rt;
			rt = t[rt].son[0];
			t[rt].fa = 0;
			t[u] = Node();
			return;
		}
		int u = rt, x = pre();
		t[t[u].son[1]].fa = x;
		t[x].son[1] = t[u].son[1];
		t[u] = Node();
		pushup(rt);
	}
}splay;

int main() {
	for(scanf("%d", &n);n;n--) {
		int op, x;
		scanf("%d%d", &op, &x);
		if(op == 1) splay.insert(x);
		else if(op == 2) splay.erase(x);
		else if(op == 3) printf("%d\n", splay.rk(x));
		else if(op == 4) printf("%d\n", splay.kth(x));
		else {
			splay.insert(x);
			printf("%d\n", splay.t[op==5?splay.pre():splay.suc()].val);
			splay.erase(x);
		}
	}
	return 0;
}

文艺平衡树

咕咕咕。