「学习笔记」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\) 节点进行旋转操作:
分析一下旋转操作:
首先设需要旋转的节点为 \(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\))
-
如果 \(x\) 的父亲不是根节点,且 \(x\) 和 \(x\) 的父亲的父亲的儿子类型相同,首先将 \(x\) 的父亲的父亲左旋或右旋,然后将 \(x\) 右旋或左旋。(图 \(3,4\))
-
如果 \(x\) 的父亲不是根节点,且 \(x\) 和 \(x\) 的父亲的父亲的儿子类型不同,将 \(x\) 左旋再右旋、或者右旋再左旋。(图 \(5,6\))
经过这 \(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;
}