【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\)
(后记:不想补了!咕咕咕)