Splay
前置芝士
熟练掌握二叉排序树的操作,了解 \(Treap\) 的左旋和右旋。
引言
\(Treap\) 巧妙地使用随机数,解决了二叉查找树保持平衡的问题。但随机数的不稳定,导致它在极小概率的情况下不能保持树的平衡。故我们需要一种更加稳定的数据结构(虽然它不是很好写)。
正文
\(Splay\) ,又名分裂树,是一种二叉排序树,它能在 \(O(log_n)\) 内完成插入、查找和删除操作。它由丹尼尔·斯立特 \(Daniel Sleator\) 和 罗伯特·恩卓·塔扬 \(Robert Endre Tarjan\) 在1985年发明的。1
\(Splay\) 的变量
\(Splay\) 的每个节点 \(x\) 有如下七种变量:
\(val_x\) : 该节点的值。
\(siz_x\) : 该节点的子树包含的节点数量;
\(sons_{x, 0}、sons_{x, 1}\): 该节点的左右子节点;
\(fa_x\) : 该节点的父亲节点;
\(num_x\) : \(val_x\) 的数量;
具体操作
初始化节点
即将该节点上述5个变量的值清空。
void init (int x) {
fa[x] = siz[x] = num[x] = val[x] = sons[x][0] = sons[x][1] = 0;
return ;
}
更新节点
更新该节点的子树包含的节点数( \(siz\) 数组)。
void pushup (int x) {
siz[x] = siz[sons[x][0]] + siz[sons[x][1]] + num[x];
return ;
}
判断节点与父亲的关系
int get (int x) {
return sons[fa[x]][1] == x;
}
连接节点
设要将 \(x\) 连接到 \(y\) 的 \(z\) 儿子上(0为左儿子,1为右儿子,下同)。
void link (int x, int y, int z) {
if (x)
fa[x] = y;
if (y)
sons[y][z] = x;
return ;
}
旋转
此处的旋转和 \(Treap\) 中的旋转有略微的不同。\(Treap\) 中的旋转是将当前节点挪至其原位置的左儿子或右儿子中,而 \(Splay\) 的旋转是将当前节点上移,但旋转的过程大致相同。
设 \(f\) 为当前节点x的父亲,\(ff\) 为 \(f\) 的父亲,\(d1\) 为 \(f\) 与 \(x\) 的关系(左儿子为0,右儿子为1),\(d2\) 为 \(ff\) 与 \(f\) 的关系。先将 \(x\) 的 \(d1\) ^ 1儿子连接到 \(f\) 点的 \(d1\) 儿子,再将 \(ff\) 连接到 \(f\) 的 \(d1\) ^ 1 儿子上,最后将已经移至原 \(f\) 位置的 \(x\) 点连接到 \(ff\) 点的 \(d2\) 儿子上。
这是笔者在学习 \(Splay\) 时自己手绘的上旋步骤,可以方便读者更好地理解 \(Splay\) 的上旋操作。
void rotate (int x) {
int f = fa[x], ff = fa[f], d1 = get (x), d2 = get (f);
link (sons[x][d1 ^ 1], f, d1);
link (f, x, d1 ^ 1);
link (x, ff, d2);
pushup (f);
pushup (x);
return ;
}
\(splay\)
\(splay\) 操作是 \(Splay\) 平衡树的核心,它保证了在经过插入删除等操作后,树的平衡性和随机性不受到破坏。(这里的 \(splay\) ,非指数据结构名,而是该数据结构的一种将某节点上旋到规定节点的一种操作。以下用" \(Splay\) "指数据结构名," \(splay\) "指 \(Splay\) 的连续上旋操作)。
设目前需要上旋的节点为 \(x\) ,\(x\) 的父亲节点为 \(f\) 。现要将该点上旋,分三种情况讨论:
1、\(f\) 为目标点。此时可以直接上旋;
2、\(f\) 与 \(f\) 的父亲节点的关系和 \(x\) 与 \(f\) 的关系相同(即 \(get(x) = get (f)\) )。此时,先上旋 \(f\) ,再上旋 \(x\);
3、\(f\) 与 \(f\) 的父亲节点的关系和 \(x\) 与 \(f\) 的关系不同(即 \(get(x) ≠ get (f)\) )。此时,连续上旋两次 \(x\) ;
若模拟上述操作可发现,在使用这种方式进行上旋的时候,树的平衡性并没有遭到破坏,因此 \(Splay\) 可以随意对树的任意节点使用。无特殊情况下,为了方便操作,一般直接旋到根节点。
void splay (int x, int top = 0) {
for (int now; (now = fa[x]) && now != top; rotate (x))
if (fa[now])
rotate (get (now) == get (x) ? now : x);
if (!top)
rt = x;
return ;
}
求根节点前缀
因为二叉排序树的性质,直接求左子树的最大值即可。
int pre () {
int now = sons[rt][0];
while (sons[now][1])
now = sons[now][1];
return now;
}
求根节点后继
因为二叉排序树的性质,直接求右子树的最小值即可。
int suc () {
int now = sons[rt][1];
while (sons[now][0])
now = sons[now][0];
return now;
}
求某数(\(x\))的排名
从根节点向下遍历,若 \(x\) 大于当前节点的值,则在最终返回答案中加上 左子树的节点数 和 当前节点的同值的数,并查询右儿子;若 \(x\) 小于当前节点的值,则直接查询左儿子。
int find (int x) {
int now = rt, ret = 0;
while (1)
if (x == val[now]) {
ret += siz[sons[now][0]] + 1;
splay (now);
return ret;
} else if (x < val[now]) {
now = sons[now][0];
continue ;
} else {
ret += siz[sons[now][0]] + num[now];
now = sons[now][1];
}
return ret;
}
查询某排名的数
和 \(Treap\) 的操作相同,可以直接查看我在 \(Treap\) 中的教程。此处不再赘述。
int ranks (int x) {
int now = rt;
while (1)
if (x <= siz[sons[now][0]])
now = sons[now][0];
else if (x > siz[sons[now][0]] + num[now]) {
x -= (siz[sons[now][0]] + num[now]);
now = sons[now][1];
} else {
splay (now);
return val[now];
}
}
插入操作
和 \(Treap\) 中的插入操作几乎相同。但是和 \(Treap\) 不同的是,不需要使用随机数进行保持平衡,而是在每次插入完成后直接将插入的点 \(Splay\) 到根节点,以此用一种很暴力的方式来保持树的平衡。
void ins (int x) {
if (!rt) {
rt = ++ cnt;
num[rt] = siz[rt] = 1;
val[rt] = x;
return ;
}
int now = rt, f = 0;
while (1) {
if (val[now] == x) {
num[now] ++;
pushup (now);
pushup (f);
splay (now);
return ;
}
f = now;
now = sons[now][val[now] < x];
if (!now) {
now = ++ cnt;
num[now] = siz[now] = 1;
val[now] = x;
fa[now] = f;
sons[f][val[f] < x] = now;
pushup (f);
splay (now);
return ;
}
}
return ;
}
删除操作
首先运行一遍 \(find\) 函数,不过不是为了寻找排名,而是将需删除的节点移至根节点,方便在删除结点之后的连边操作。其他操作与 \(Treap\) 相同。
void del (int x) {
find (x);
if (num[rt] > 1) {
num[rt] --;
pushup (rt);
return ;
}
if (!sons[rt][0] && !sons[rt][1]) {
init (rt);
rt = 0;
return ;
}
if (!sons[rt][0] && sons[rt][1]) {
int t = rt;
rt = sons[rt][1];
fa[rt] = 0;
init (t);
return ;
}
if (sons[rt][0] && !sons[rt][1]) {
int t = rt;
rt = sons[rt][0];
fa[rt] = 0;
init (t);
return ;
}
if (sons[rt][0] && sons[rt][1]) {
int p1 = rt, p2 = pre ();
splay (p2);
link (sons[p1][1], rt, 1);
init (p1);
pushup (rt);
return ;
}
}
整体代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, m, cnt, rt, sons[N][2], fa[N], val[N], siz[N], num[N];
void init (int x) {
fa[x] = siz[x] = num[x] = val[x] = sons[x][0] = sons[x][1] = 0;
return ;
}
int get (int x) {
return sons[fa[x]][1] == x;
}
void link (int x, int y, int z) {
if (x)
fa[x] = y;
if (y)
sons[y][z] = x;
return ;
}
void pushup (int x) {
siz[x] = siz[sons[x][0]] + siz[sons[x][1]] + num[x];
return ;
}
void rotate (int x) {
int f = fa[x], ff = fa[f], d1 = get (x), d2 = get (f);
link (sons[x][d1 ^ 1], f, d1);
link (f, x, d1 ^ 1);
link (x, ff, d2);
pushup (f);
pushup (x);
return ;
}
void splay (int x, int top = 0) {
for (int now; (now = fa[x]) && now != top; rotate (x))
if (fa[now])
rotate (get (now) == get (x) ? now : x);
if (!top)
rt = x;
return ;
}
void ins (int x) {
if (!rt) {
rt = ++ cnt;
num[rt] = siz[rt] = 1;
val[rt] = x;
return ;
}
int now = rt, f = 0;
while (1) {
if (val[now] == x) {
num[now] ++;
pushup (now);
pushup (f);
splay (now);
return ;
}
f = now;
now = sons[now][val[now] < x];
if (!now) {
now = ++ cnt;
num[now] = siz[now] = 1;
val[now] = x;
fa[now] = f;
sons[f][val[f] < x] = now;
pushup (f);
splay (now);
return ;
}
}
return ;
}
int find (int x) {
int now = rt, ret = 0;
while (1)
if (x == val[now]) {
ret += siz[sons[now][0]] + 1;
splay (now);
return ret;
} else if (x < val[now]) {
now = sons[now][0];
continue ;
} else {
ret += siz[sons[now][0]] + num[now];
now = sons[now][1];
}
return ret;
}
int ranks (int x) {
int now = rt;
while (1)
if (x <= siz[sons[now][0]])
now = sons[now][0];
else if (x > siz[sons[now][0]] + num[now]) {
x -= (siz[sons[now][0]] + num[now]);
now = sons[now][1];
} else {
splay (now);
return val[now];
}
}
int pre () {
int now = sons[rt][0];
while (sons[now][1])
now = sons[now][1];
return now;
}
int suc () {
int now = sons[rt][1];
while (sons[now][0])
now = sons[now][0];
return now;
}
void del (int x) {
find (x);
if (num[rt] > 1) {
num[rt] --;
pushup (rt);
return ;
}
if (!sons[rt][0] && !sons[rt][1]) {
init (rt);
rt = 0;
return ;
}
if (!sons[rt][0] && sons[rt][1]) {
int t = rt;
rt = sons[rt][1];
fa[rt] = 0;
init (t);
return ;
}
if (sons[rt][0] && !sons[rt][1]) {
int t = rt;
rt = sons[rt][0];
fa[rt] = 0;
init (t);
return ;
}
if (sons[rt][0] && sons[rt][1]) {
int p1 = rt, p2 = pre ();
splay (p2);
link (sons[p1][1], rt, 1);
init (p1);
pushup (rt);
return ;
}
}
int main () {
scanf ("%d", &n);
for (int i = 1; i <= n; i ++) {
int opt, x;
scanf ("%d%d", &opt, &x);
if (opt == 1)
ins (x);
else if (opt == 2)
del (x);
else if (opt == 3)
printf ("%d\n", find (x));
else if (opt == 4)
printf ("%d\n", ranks (x));
else if (opt == 5) {
ins (x);
printf ("%d\n", val[pre ()]);
del (x);
} else if (opt == 6) {
ins (x);
printf ("%d\n", val[suc ()]);
del (x);
}
}
return 0;
}