【数据结构】Splay 树

Splay

Splay 树(伸展树),是平衡树的一种,而且可以维护序列,进行一些序列操作。有些序列操作功能甚至是线段树实现不了的(经典的区间翻转)。
维护集合时,Splay 的中序遍历单调递增,而维护序列时,Splay 的中序遍历是维护的序列。
Splay 通过均摊(势能分析)来保证复杂度正确,单次插入,删除,查找操作复杂度为 \(O(\log n)\)

基本思想

Splay 均摊复杂度的基本思想是:每次操作一个节点,都把这个节点通过 Splay 操作伸展到根节点。

写在前面

Splay 代码实现上的一些技巧。

const int N = 1e5 + 5;

struct node
{
    int ch[2], fa;    // 左右孩子节点,父亲节点的编号
    int val, cnt, sz; // 当前节点的值,当前节点有多少个,当前子树的大小
#define lc (t[p].ch[0])
#define rc (t[p].ch[1])
} t[N];
int tot, root;
int new_node(int val) // 新建节点
{
    int x = ++tot;
    t[x].val = val, t[x].cnt = t[x].sz = 1;
    return x;
}
int get(int p) // 判断节点 p 是其父亲的左子节点或右子节点
{
    return p == t[t[p].fa].ch[1];
}
void push_up(int p) // 从下向上传递信息
{
    t[p].sz = t[lc].sz + t[rc].sz + t[p].cnt;
}
void clear(int p) // 清空一个节点
{
    t[p].ch[0] = t[p].ch[1] = t[p].fa = t[p].val = t[p].cnt = t[p].sz = 0;
}
void connect(int p,int fa,int k) // 把节点 p 设置为节点 fa 的左儿子或右儿子  
{
    t[p].fa = fa, t[fa].ch[k] = p;
}

旋转

Splay 和 Treap 的旋转操作类似但更丰富。可以分为单旋和双旋,双旋又可分为同构调整和异构调整。

单旋

分为左旋与右旋,和 Treap 类似。直接看图:
OI Wiki 平衡树旋转
AGOH 小口诀:“左旋拎右左挂右,右旋拎左右挂左”
代码实现上,发现左旋右旋可以通过异或操作来简化成同一个函数。

void rotate(int p) // 单旋
{
    int fa = t[p].fa, ffa = t[fa].fa, k = get(p);
    connect(p, ffa, get(fa));
    connect(t[p].ch[k ^ 1], fa, k);
    connect(fa, p, k ^ 1);
    push_up(fa);
    push_up(p);
}

双旋

OI Wiki 的图中,记要伸展(双旋)的节点为 \(x\),其父节点与父节点的父节点分别称作 \(p\)\(g\)

同构调整

\(p\) 不是根节点,且 \(x\)\(p\) 同时是其父节点的左子节点或右子节点时,对 \(p\) 的双旋是同构调整。如下图。
同构调整先单旋 \(p\) 再单旋 \(x\)

zig-zig - OI Wiki

异构调整

\(p\) 不是根节点,且 \(x\)\(p\) 不同时是其父节点的左子节点或右子节点时,对 \(p\) 的双旋是异构调整。如下图。
异构调整单旋两次 \(x\)

zig-zag - OI Wiki

伸展

Splay 树的基本思想就是每次操作后要把操作的节点伸展到根节点。伸展操作的基本思路就是不断进行双旋,直到旋转到正确的位置。最后一次可以进行单旋进行奇偶校验。

伸展操作的代码实现:

void splay(int p, int k) // 把节点 p 伸展到 k 的子节点,k = 0 时表示伸展到根节点
{
    if (!k)
        root = p;
    while (t[p].fa != k)
    {
        if (t[t[p].fa].fa != k) get(p) ^ get(t[p].fa) ? rotate(p) : rotate(t[p].fa);
        rotate(p);
    }
}

二叉搜索树操作

下面介绍一下其余一些二叉搜索树的常用操作,和 Treap 等其他平衡树和朴素 BST 类似,只是操作后要进行 Splay 操作。

插入

void insert(int &p, int val, int fa) // 插入节点
{
    if (!p)
    {
        p = new_node(val);
        t[p].fa = fa;
        splay(p, 0);
        return;
    }
    else if (val < t[p].val)
        insert(lc, val, p);
    else if (val > t[p].val)
        insert(rc, val, p);
    else
        t[p].cnt++;
    push_up(p);
}

找前驱后继

int get_prev(int p, int val) // 找前驱节点
{
    if (!p) return -1;
    if (t[p].val >= val) return get_prev(lc, val);
    int r = get_prev(rc, val);
    if (r == -1 || t[p].val > t[r].val) {
        if (p == root) splay(p, 0);
        return p;
    }
    else {
        if (p == root) splay(r, 0);
        return r;
    }
    // return max(t[p].val, get_prev(rc, val));
}
int get_next(int p, int val) // 找后继节点z
{
    if (!p) return -1;
    if (t[p].val <= val) return get_next(rc, val);
    // return min(t[p].val, get_next(lc, val));
    int l = get_next(lc, val);
    if (l == -1 || t[p].val < t[l].val) {
        if (p == root) splay(p, 0);
        return p;
    }
    else {
        if (p == root) splay(l, 0);
        return l;
    }
}

删除

首先把要删除的节点伸展到根节点,然后如果当前节点大于 1 个则减 1 即可,否则删去这个节点,然后合并两棵子树即可。

代码实现

这题的代码我咋调都调不出来,气死我了,不调了,放个记录在这里,好心人看到帮我调一下。
loj 提交记录

维护信息

Splay 也可以用类似线段树的方法维护信息,方法也是使用延迟标记。
看一个经典的例子(维护区间翻转):
loj #105. 文艺平衡树

posted @ 2023-10-23 19:34  蒟蒻OIer-zaochen  阅读(274)  评论(0编辑  收藏  举报