Splay 学习笔记 [基础]


Splay

前言

平衡树这方面,我是从 \(\text{Treap}\) 入门的。但是总觉得 \(\text{Treap}\) 左右旋转不是很好,就去学了 \(\text{FHQ-Treap}\)。自从学了这个非旋转的 \(\text{Treap}\) 之后,它的简洁和优美就令我震撼。大部分题已经可以做了!不过平衡树这个数据结构里面一个很常用的优秀平衡树——\(\text{Splay}\) 我还没有学。鉴于以后会有用到 \(\text{Splay}\) 作为前置知识的数据结构,就学了一下 \(\text{Splay}\)

不过 \(\text{Splay}\) 真的比 \(\text{FHQ-Treap}\) 复杂很多

参考资料:\(\text{OI wiki}\)

本博客的两版代码都通过了 \(\text{LibreOJ}\) 的模板题。放上来的代码也是它格式化过的。

定义

首先是定义部分呢。

fa[N]  ch[N][2]  val[N]   cnt[N]      sz[N]   rt    tot 
父节点  左右儿子   节点的值 相同值节点计数  子树大小 根节点 总节点个数

基本函数

一些维护基本信息的函数。

void Maintain(int x) {
    sz[x] = sz[ch[x][0]] + sz[ch[x][1]] + cnt[x];
} // 维护节点大小
bool Get(int x) {
    return x == ch[fa[x]][1];
} // 判断是左儿子还是右儿子,左儿子返回 0,右儿子返回 1
void Clear(int x) {
    ch[x][0] = ch[x][1] = fa[x] = val[x] = sz[x] = cnt[x] = 0;
}

旋转

\(\text{Splay}\) 的旋转分左右旋。但是在 \(\text{Rotate}\) 这个函数的编写上,旋转变成了自适应旋转。一次旋转的结果就是把被旋转的节点往根的方向上提一个节点。

void Rotate(int x) {
    int y = fa[x], z = fa[y], chk = Get(x);
    ch[y][chk] = ch[x][chk ^ 1]; // y 的 x 方向的儿子变成 x 的另一方向的儿子

    if (ch[x][chk ^ 1])
        fa[ch[x][chk ^ 1]] = y; // 要是 x 另一方向真的有儿子的话,更新父亲

    ch[x][chk ^ 1] = y; // x 另一方向儿子变成 y
    fa[y] = x; // y 的父亲变成 x
    fa[x] = z; // x 的父亲变成 z

    if (z)
        ch[z][y == ch[z][1]] = x; // 要是真的有 z 的话,z 的 y 方向的儿子变成 x

    Maintain(y);
    Maintain(x);
}

Splay

这个操作是 \(\text{Splay}\) 能保持平衡的关键。它把一个选定节点一路旋转到根节点。

由于我们的旋转是自适应旋转,双旋不用讨论左右,只需要讨论是转两次 \(x\) 节点,或者是先转一次 \(fa(x)\) 再转一次 \(x\) 即可。

void Splay(int x) {
    for (int f = fa[x]; f = fa[x], f; Rotate(x))
        if (fa[f])
            Rotate(Get(x) == Get(f) ? f : x);

    rt = x;
    // 相同就转父亲,不同就转 x,之后循环体再转一次 x
}

插入

\(\text{Splay}\) 的插入就需要稍微讨论一下。

主要是需要记住在操作结束之后进行一发 Splay 操作。

void Insert(int k) {
    if (!rt) { // 没有根直接插入
        val[++ tot] = k;
        cnt[tot] ++;
        rt = tot;
        Maintain(rt);
        return;
    }

    int cur = rt, f = 0; // 当前位置为根

    while (true) {
        if (val[cur] == k) { // 已有节点,增加计数即可
            cnt[cur] ++;
            Maintain(cur);
            Maintain(f);
            Splay(cur);
            break;
        }

        f = cur; // f(父节点) = 当前节点
        cur = ch[cur][val[cur] < k]; // 当前节点往下走

        if (!cur) { // 当前节点为空,插入
            val[++ tot] = k;
            cnt[tot] ++;
            fa[tot] = f;
            ch[f][val[f] < k] = tot;
            Maintain(tot);
            Maintain(f);
            Splay(tot);
            break;
        }
    }
}

根据值求排名

还是 BST 那一套操作。也需要注意在最后来一发 Splay

int Rank(int k) {
    int res = 0, cur = rt;

    while (true) {
        if (k < val[cur])
            cur = ch[cur][0]; // 走进左子树
        else {
            res += sz[ch[cur][0]];

            if (k == val[cur]) { // 找到了
                Splay(cur);
                return res + 1;
            }

            res += cnt[cur];
            cur = ch[cur][1];
            // 走到右子树
        }
    }
}

根据排名求值

还是需要注意 Splay 操作。

int Kth(int k) {
    int cur = rt;

    while (true) {
        if (ch[cur][0] && k <= sz[ch[cur][0]])
            cur = ch[cur][0]; // 走到左子树
        else {
            k -= cnt[cur] + sz[ch[cur][0]]; // 减去当前和左子树的

            if (k <= 0) {
                Splay(cur);
                return val[cur]; // 找到了
            }

            cur = ch[cur][1]; // 走到右子树
        }
    }
}

求前驱后继

\(\text{Splay}\) 的求前驱和后继就显得很累赘,但是能很好地体现出它独有的 Splay 操作的特色。

在执行求 \(x\) 的前驱或后继的函数之前,要先插入 \(x\),这样 \(x\) 就被转到了根。之后执行 Pre() 或者 Nxt(),最后还得 Del(x)\(x\) 这个值再删掉。

但是相对来说会简单一点,比起 \(\text{Treap}\)。不过还是比不上 \(\text{FHQ-Treap}\)。由于根节点一定为 \(x\),所以前驱直接从左子树往右走,后继直接从右子树往左走。

int Pre() { // 前驱:先插入,再调用这个函数,再删除
    int cur = ch[rt][0]; // 左子树

    if (!cur)
        return cur;

    while (ch[cur][1])
        cur = ch[cur][1]; // 最右边的

    Splay(cur);
    return cur;
}
int Nxt() { // 同理
    int cur = ch[rt][1];

    if (!cur)
        return cur;

    while (ch[cur][0])
        cur = ch[cur][0];

    Splay(cur);
    return cur;
}

删除

在求前驱和后继的时候,我们就先提到了“删除”这个函数。

删除也还挺麻烦的...分情况讨论。

void Del(int k) {
    Rank(k); // 把值为 k 的节点转到根

    if (cnt[rt] > 1) {
        cnt[rt] --;
        Maintain(rt);
        return; // 直接减少计数,不用破坏树的形态
    }

    if (!ch[rt][0] && !ch[rt][1]) {
        Clear(rt);
        rt = 0;
        return; // 只剩根,清掉即可
    }

    if (!ch[rt][0]) {
        int cur = rt;
        rt = ch[rt][1];
        fa[rt] = 0;
        Clear(cur);
        return; // 没有左儿子,把根设置为右儿子,删掉根即可
    }

    if (!ch[rt][1]) {
        int cur = rt;
        rt = ch[rt][0];
        fa[rt] = 0;
        Clear(cur);
        return; // 同理
    }

    int cur = rt;
    int x = Pre();
    fa[ch[cur][1]] = x;
    ch[x][1] = ch[cur][1];
    Clear(cur);
    Maintain(rt);
    // 有左右儿子,还得拆掉根节点,就先找前驱
    // 之后根的右儿子的父亲设置为 x
    // x 的右儿子设置为根的右儿子
    // 类似链表,根的信息就没了联系
    // 清掉根的信息,维护根即可
}

封装

可以使用结构体进行一个封装。

这里是封装后,模板题的代码:(结合个人风格,使用了宏定义)

#include 
#define ch(x,y) ch[(x)][(y)]
using namespace std;
const int N = 100005;
int rt, tot;
int sz[N], ch[N][2], val[N], fa[N], cnt[N];
struct SplayTemplate {
    void Maintain(int x) {
        sz[x] = sz[ch(x, 0)] + sz[ch(x, 1)] + cnt[x];
    }
    bool Get(int x) {
        return x == ch(fa[x], 1);
    }
    void Clear(int x) {
        sz[x] = ch(x, 0) = ch(x, 1) = val[x] = cnt[x] = fa[x] = 0;
    }
    //--------------------------------------------------------------
    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;

        Maintain(y);
        Maintain(x);
    }
    void Splay(int x) {
        for (int f = fa[x]; f = fa[x], f; Rotate(x))
            if (fa[f])
                Rotate(Get(f) == Get(x) ? f : x);

        rt = x;
    }
    void Insert(int k) {
        if (!rt) {
            val[++ tot] = k;
            cnt[tot] ++;
            rt = tot;
            Maintain(rt);
            return;
        }

        int cur = rt, f = 0;

        while (1) {
            if (val[cur] == k) {
                cnt[cur] ++;
                Maintain(cur);
                Maintain(f);
                Splay(cur);
                break;
            }

            f = cur;
            cur = ch(cur, val[cur] < k);

            if (!cur) {
                val[++ tot] = k;
                cnt[tot] ++;
                fa[tot] = f;
                ch(f, val[f] < k) = tot;
                Maintain(tot);
                Maintain(f);
                Splay(tot);
                break;
            }
        }
    }
    int Rank(int k) { // 给值求排名
        int res = 0, cur = rt;

        while (1) {
            if (k < val[cur])
                cur = ch(cur, 0);
            else {
                res += sz[ch(cur, 0)];

                if (k == val[cur]) {
                    Splay(cur);
                    return res + 1;
                }

                res += cnt[cur];
                cur = ch(cur, 1);
            }
        }
    }
    int Kth(int k) { // 给排名求值
        int cur = rt;

        while (1) {
            if (ch(cur, 0) && k <= sz[ch(cur, 0)])
                cur = ch(cur, 0);
            else {
                k -= cnt[cur] + sz[ch(cur, 0)];

                if (k <= 0) {
                    Splay(cur);
                    return val[cur];
                }

                cur = ch(cur, 1);
            }
        }
    }
    int Pre() {
        int cur = ch(rt, 0);

        if (!cur)
            return cur;

        while (ch(cur, 1))
            cur = ch(cur, 1);

        Splay(cur);
        return cur;
    }
    int Nxt() {
        int cur = ch(rt, 1);

        if (!cur)
            return cur;

        while (ch(cur, 0))
            cur = ch(cur, 0);

        Splay(cur);
        return cur;
    }
    void Del(int k) {
        Rank(k);

        if (cnt[rt] > 1) {
            cnt[rt] --;
            Maintain(rt);
            return;
        }

        if (!ch(rt, 0) && !ch(rt, 1)) {
            Clear(rt);
            rt = 0;
            return;
        }

        if (!ch(rt, 0)) {
            int cur = rt;
            rt = ch(rt, 1);
            fa[rt] = 0;
            Clear(cur);
            return;
        }

        if (!ch(rt, 1)) {
            int cur = rt;
            rt = ch(rt, 0);
            fa[rt] = 0;
            Clear(cur);
            return;
        }

        int cur = rt;
        int x = Pre();
        fa[ch(cur, 1)] = x;
        ch(x, 1) = ch(cur, 1);
        Clear(cur);
        Maintain(rt);
    }
} SplayTree;

int main() {
    //freopen("Splay.in","r",stdin);
    int n;
    scanf("%d", &n);

    for (int i = 1, x, opt; i <= n; i ++) {
        scanf("%d%d", &opt, &x);

        if (opt == 1)
            SplayTree.Insert(x);
        else if (opt == 2)
            SplayTree.Del(x);
        else if (opt == 3)
            printf("%d\n", SplayTree.Rank(x));
        else if (opt == 4)
            printf("%d\n", SplayTree.Kth(x));
        else if (opt == 5)
            SplayTree.Insert(x), printf("%d\n", val[SplayTree.Pre()]), SplayTree.Del(x);
        else if (opt == 6)
            SplayTree.Insert(x), printf("%d\n", val[SplayTree.Nxt()]), SplayTree.Del(x);
    }

    return 0;
}

后记

要是单纯写个平衡树,打死我我都不写 \(\text{Splay}\)(误)

还是推荐写 \(\text{FHQ-Treap}\),好写好调。

不过,既然是平衡树,肯定都不是那么容易