『笔记』Splay(一)

写在前面

\(SPLAY\) ??


SPLAY

定义

百度者云:

伸展树$ (Splay Tree)$ ,也叫分裂树,是一种二叉排序树,它能在 \(O(log n)\) 内完成插入、查找和删除操作。

在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠*树根的位置。于是想到设计一个简单方法, 在每次查找之后对树进行重构,把被查找的条目搬移到离树根*一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

它的优势在于不需要记录用于*衡树的冗余信息。

啥玩意?

学长者言:

\(Splay\) 是一种二叉搜索树的维护方式。它通过不断将某节点旋转到根节点,使得整棵树仍满足二叉搜索树的性质,且保持*衡不至于退化为链,以保证复杂度。

思想

对于查找频率较高的节点,使其处于离根节点相对较*的节点

那什么是所谓查找频率较高的节点?

我们可以将每次操作的节点看作是查找频率相对较高的节点,注意这里是相对。

那么如何实现把该节点转移到根的操作呢?

可以对树进行旋转操作。

旋转

功能

用于将指定的节点上移一个位置。

实现

旋转操作分为左旋和右旋两种,顾名思义就是向左旋转和向右旋转,手玩模拟就是顺时针和逆时针旋转。

二者的实现思路和目的皆相同,只不过是指定节点为左孩子时使用右旋,右孩子时使用左旋。


by Luckyblock

在进行旋转操作时,应遵循一下原则:

  • 整棵树的中序遍历不变,即不破坏二叉搜索树树的基本性质

  • 节点维护的信息依然正确有效

  • \(root\) 必须指向旋转后的根节点

其实类似于 Treap 的旋转操作,只是多了对 fa 数组的维护。

当 x 是 y 的左孩子时

我们如果让 \(x\) 成为 y 的父亲,只会影响到三个点的关系,即 \(r\)\(x\)\(x\)\(y\)\(x\)\(r\)

根据二叉树的性质,

\(r\) 会成为 \(y\) 的左儿子;

\(y\) 会成为 \(x\) 的右儿子;

\(x\) 会成为 \(r\) 的儿子,左右依据 \(y\)\(r\) 的什么儿子;

然后就变成这么个样:

当 x 是 y 的右孩子时

转化过程与上面的一样:

代码

void zig(int now) //右旋
{
    int fa_ = fa[now];
    int z = fa[fa_];

    lc[fa_] = rc[now];
    fa[rc[now]] = fa_;
    rc[now] = fa_;
    fa[fa_] = now, fa[now] = z;

    if (fa_ == lc[z])
        lc[z] = now;
    else if (fa_ == rc[z])
        rc[z] = now;

    update(fa_);
    update(now);

    if (rt == fa_)
        rt = now;
}

void zag(int now) //左旋
{
    int fa_ = fa[now];
    int z = fa[fa_];

    rc[fa_] = lc[now];
    fa[lc[now]] = fa_;
    lc[now] = fa_;
    fa[fa_] = now, fa[now] = z;

    if (fa_ == lc[z])
        lc[z] = now;
    else if (fa_ == rc[z])
        rc[z] = now;

    update(fa_);
    update(now);

    if (rt == fa_)
        rt = now;
}

写代码的过程中我们发现,第二种情况就是吧第一种情况的 \(x\)\(y\) 调换了位置,那我们能不能将张两种情况合并起来实现呢?

既然要实现上述功能,就必须明确当前是他的父节点的左孩子还是右孩子

int check_son(int now_)//获得儿子类型
{
    return now_ == son[fa[now_]][1];
}
/*
如果是
    左孩子返回 true
    右孩子返回 false
*/

那么单旋函数就可以写出来了:

void rotate(int now_)
{
    int fa_ = fa[now_];
    bool chson = check_son(now_);

    if (fa[fa[now_]])
        son[fa[fa[now_]]][check_son(fa[now_])] = now_; //更新祖父
    fa[now_] = fa[fa[now_]];

    son[fa_][chson] = son[now_][chson ^ 1]; //更新 fa 的儿子
    fa[son[fa_][chson]] = fa_;

    son[now_][chson ^ 1] = fa_; //上提 now_
    fa[fa_] = now_;

    push_up(fa_);
    push_up(now_);
}

伸展操作

定义

\(Splay\) 利用伸展操作趋**衡。

也就是说 \(Splay\) 实现将但钱访问过的节点旋转至根节点的位置。

功能

这样可以使经常访问的节点访问的速度优化得非常快(因为节点几乎完全在根节点附*),而且在旋转的过程中整棵树趋*于*衡。

实现

两种方式,单旋双旋

最简单的办法,对于 \(x\) 这个节点,不断地上提该节点,直到它到达 \(to\),这种方式叫做单旋

但是如果某题的出题人极度不友好,那么单旋操作的时间复杂度就有可能直接起飞。

那么就有了双旋操作。

双旋操作要具体讨论当 前节点 \(-\) 父节点的关系 与 当前节点的父节点 \(-\) 祖父节点的关系 是否相同。

单旋

直接无脑上提某节点即可。

双旋

设访问过的节点为 \(x\)\(x\) 的父节点为 \(fa[x]\) ,则有三种情况:

  1. \(fa[x]\) 为根节点。

    \(x\) 旋转一次即可。

  2. \(fa[x]\) 不是根节点,\(x\)\(fa[x]\) / 儿子且 \(fa[x]\)\(fa[fa[x]]\) / 儿子。

    先右 / 左旋 \(fa[x]\) ,再右 / 左旋 \(x\)

  3. \(fa[x]\) 不是根节点,\(x\)\(fa[x]\) / 儿子且 \(fa[x]\)\(fa[fa[x]]\) / 儿子。

    先右 / 左旋 \(x\) ,再左 / 右旋 \(x\)

容易发现,在直接的链状数据结构上,每次双旋都会产生一个交叉点,而使得某段的高度下降一般从而最后使得整个树的高度减少了一般,最终倒是二叉搜索树趋于*衡。

CODE

Splay(x,to) 表示把 \(x\) 旋转到 \(to\) 的儿子(当 \(g=0\) 时表示旋转到根)

void rotate(int now_)//通过旋转把节点 now_ 上移
{
    int fa_ = fa[now_];
    bool chson = check_son(now_);

    if (fa[fa[now_]])
        son[fa[fa[now_]]][check_son(fa[now_])] = now_; //更新祖父
    fa[now_] = fa[fa[now_]];

    son[fa_][chson] = son[now_][chson ^ 1]; //更新 fa 的儿子
    fa[son[fa_][chson]] = fa_;

    son[now_][chson ^ 1] = fa_; //上提 now_
    fa[fa_] = now_;

    push_up(fa_);
    push_up(now_);
}

void splay(int now_, int to) //把 now_ 旋转到 to 的儿子的位置(当 to=0 时表示旋转到根)
{
    while (fa[now_] != to)
    {
        int f = fa[now_];
        int f_ = fa[f];
        if (f_ != to)
            rotate((now_ == lc[f]) == (f == lc[f_]) ? f : now_);
        rotate(now_);
    }
    if (!to)
        rt = now_; //标记根节点
}

封装模板

By Luckyblock

太爱了学长了鸭!!

GYH 贴贴~

//by Luckyblock

namespace Splay
{
    #define f fa[now_]
    #define ls son[now_][0]
    #define rs son[now_][1]

    const int kMaxNode = 1e6 + 10;

    int root, node_num, fa[kMaxNode], son[kMaxNode][2];
    int val[kMaxNode], cnt[kMaxNode], siz[kMaxNode];
    int top, bin[kMaxNode];

    void Pushup(int now_) //更新节点大小信息
    {
        if (!now_)
            return;
        siz[now_] = cnt[now_];
        if (ls)
            siz[now_] += siz[ls];
        if (rs)
            siz[now_] += siz[rs];
    }
    int WhichSon(int now_) //获得儿子类型
    {
        return now_ == son[f][1];
    }
    void Clear(int now_) //清空节点,同时进行垃圾回收
    {
        f = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
        bin[++top] = now_;
    }
    int NewNode(int fa_, int val_) //建立新节点
    {
        int now_ = top ? bin[top--] : ++node_num; //优先调用垃圾堆
        f = fa_, val[now_] = val_, siz[now_] = cnt[now_] = 1;
        return now_;
    }
    void Rotate(int now_) //旋转操作
    {
        int fa_ = f, whichson = WhichSon(now_);
        if (fa[f])
            son[fa[f]][WhichSon(f)] = now_;
        f = fa[f];

        son[fa_][whichson] = son[now_][whichson ^ 1];
        fa[son[fa_][whichson]] = fa_;

        son[now_][whichson ^ 1] = fa_;
        fa[fa_] = now_;
        Pushup(fa_), Pushup(now_);
    }
    void Splay(int now_) //旋转到根操作
    {
        for (; f != 0; Rotate(now_))
            if (fa[f])
                Rotate(WhichSon(now_) == WhichSon(f) ? f : now_);
        root = now_;
    }
    void Insert(int now_, int fa_, int val_) //插入操作
    {
        if (now_ && val[now_] != val_)
        {
            Insert(son[now_][val[now_] < val_], now_, val_);
            return;
        }
        if (val[now_] == val_) //已存在
            ++cnt[now_];
        if (!now_) //不存在
        {
            now_ = NewNode(fa_, val_);
            if (f)
                son[f][val[f] < val_] = now_;
        }

        Pushup(now_);
        Pushup(f);
        Splay(now_); //注意旋转到根
    }
    int Find(int now_, int val_) //将权值 val 对应节点旋转至根。在*衡树上二分。
    {
        if (!now_) //不存在
            return false;
        if (val_ < val[now_])
            return Find(ls, val_);
        if (val_ == val[now_])
        {
            Splay(now_);
            return true;
        }
        return Find(rs, val_);
    }
    void Delete(int val_) //删除一个权值 val
    {
        if (!Find(root, val_)) //将 val 转到根
            return;
        if (cnt[root] > 1)
        {
            --cnt[root];
            Pushup(root);
            return;
        }
        int oldroot = root;
        if (!son[root][0] && !son[root][1])
            root = 0;
        else if (!son[root][0])
            root = son[root][1], fa[root] = 0;
        else if (!son[root][1])
            root = son[root][0], fa[root] = 0;
        else if (son[root][0] && son[root][1]) //将中序遍历中 root 前的一个元素作为新的 root。该元素即为 root 左子树中最大的元素。
        {
            int leftmax = son[root][0];
            while (son[leftmax][1])
                leftmax = son[leftmax][1];
            Splay(leftmax); //转到根

            son[root][1] = son[oldroot][1];
            fa[son[root][1]] = root; //继承信息
        }
        Clear(oldroot);
        Pushup(root);
    }
    int QueryRank(int val_) //查询 val_ 的排名
    {
        Insert(root, 0, val_); //先插入,将其转到根,查询左子树大小
        int ret = siz[son[root][0]] + 1;
        Delete(val_);
        return ret;
    }
    int QueryVal(int rk_) //查询排名为 rk 的权值。在*衡树上二分。
    {
        int now_ = root;
        while (true)
        {
            if (!now_)
                return -1;
            if (ls && siz[ls] >= rk_) //注意 =
                now_ = ls;
            else
            {
                rk_ -= ls ? siz[ls] : 0;
                if (rk_ <= cnt[now_]) //该权值即为 val[now_]
                {
                    Splay(now_);
                    return val[now_];
                }
                rk_ -= cnt[now_];
                now_ = rs;
            }
        }
    }
    int QueryPre(int val_) //查询前驱
    {
        Insert(root, 0, val_); //插入 val_,将其旋转到根,前驱(中序遍历中前一个元素)即为左子树中最大的元素。
        int now_ = son[root][0];
        while (rs)
            now_ = rs;
        Delete(val_);
        return val[now_];
    }
    int QueryNext(int val_) //查询后继
    {
        Insert(root, 0, val_);
        int now_ = son[root][1];
        while (ls)
            now_ = ls;
        Delete(val_);
        return val[now_];
    }
}

//有删减修改

写在最后

\(Splay\) 并木有就此完结,上述模板中还有很多基本操作,且听下回分解。

\[\huge{\color{#F4F2F2}{To\ Be\ Continue~}} \]

鸣谢:

posted @ 2021-03-17 20:33  Frather  阅读(81)  评论(0编辑  收藏  举报