其他分享
首页 > 其他分享> > 数据结构

数据结构

作者:互联网

平衡树

我们需要维护一种数据结构,支持以下操作:插入值,删除值,查询数在集合中的排名,查询排名为 \(k\) 的数,求某个数的前驱与后继。

我们可以用二叉搜索树维护,但是可以被卡成 \(O(n)\) ,那么我们要用到另外一种数据结构,即平衡树来维护这些操作。

平衡树种类较多,这里仅介绍其中的Splay、Treap、FHQ Treap。

Splay

平衡树是基于BST也就是二叉搜索树的结构来进行维护的,它有一些性质。

左儿子 \(<\) 根 \(<\) 右儿子,这里的小于号可以自定义。且中序遍历可以得到一个有序序列。

我们在正常情况下显然可以用BST的特性去跳左右儿子就可以找到目标节点,但是在链数据下会从 \(O(log)\) 复杂度卡成 \(o(n)\)。

那么,Splay就是通过其独特的旋转操作,用旋学来稳定玄学的 \(O(log(n))\) 复杂度。

关于每个节点我们需要存储的信息有这些:父节点与子节点编号,权值,权值出现次数与此节点所在子树的大小。

那么我们可以写出维护其基本性质的代码:

const int Maxn = 1e5 + 10;
#define ls(x) son[x][0]
#define rs(x) son[x][1]

int fa[Maxn], son[Maxn][2];
int val[Maxn], cnt[Maxn];
int siz[Maxn];

inline void clear(int x)
{
    return ls(x) = rs(x) = f[x] = siz[x] = cnt[x] = val[x] = 0, void();
} //删除节点

inline bool get(int x)
{
    return (rs(f[x]) == x);
} //判断节点是左儿子还是右儿子

inline void up(int x)
{
    if (x)
    {
        siz[x] = cnt[x];
        ls(x) ? siz[x] += siz[ls(x)] : siz[x] = siz[x];
        rs(x) ? siz[x] += siz[rs(x)] : siz[x] = siz[x];
    }

    return;
} //上传

那么下一步就是Splay的精髓所在——旋转。

我们根据不同的情况去调整,直到我们需要的节点转到了根节点处

旋转的过程也就是 \(x\) 节点逐渐向根结点逼近的过程。那么我们每次旋转的时候去考虑将其旋转到它父节点的位置来向上逼近。

手玩一下可得,我们需要把从 \(x\) 到根节点上 \(fa[x]\) 连接的边都删掉,然后把 \(fa[x]\) 连成 \(x\) 的右儿子以维护BST性质,如果 \(x\) 点存在右子树,那么就把它连到 \(fa[x]\) 的左子树上,最后再更新一下 \(fa[fa[x]]\) 的左儿子即可。

但是在 $ x、fa[x]、fa[fa[x]] $ 共线的情况下,如果仍然按照从下往上的顺序转,那么我们的链结构会被破坏从而导致失衡。此时就需要用双旋去平衡,其实也就是先转父节点再转子节点,最终就可以达到我们想要的结果。

最后不要忘了上传。

inline void rotate(int x)
{
    int fx = f[x], gfx = f[fx];
    int c = get(ix);
    f[x] = gfx;
    f[fx] = x;
    son[fx][c] = ch[x][c ^ 1];
    son[x][c ^ 1] = fx;
    if (son[x][c ^ 1])
        fa[son[x][c ^ 1]] = fx;
    if (gfx)
        son[gfx][get(fx)] = x;
    up(fx);
    up(x);
}

inline void Splay(int x)
{
    while (fa[x])
    {
        if (fa[fa[x]])
            rotate(get(x) == get(fa[x]) ? fa[x] : x);
        rotate(x);
    }
    rt = x;
    return;
}

那么接下来的操作就好说了,我们已经维护好了性质,只需要利用我们构建的优美的平衡结构即可。

inline int rt_pre()
{
    int x = ls(rt);
    while (rs(x))
        x = rs(x);
    splay(x);
    return x;
}

inline int rt_nxt()
{
    int x = rs(x);
    while (ls(x))
        x = ls(x);
    splay(x);
    return x;
}

inline void insert(int x)
{
    int i = rt, f = 0;

    while (i and val[i] != x)
        f = i, i = son[i][x > val[i]];

    if (i)
        cnt[i]++;
    else
    {
        i = tot++;
        cnt[tot] = siz[tot] = 1;
        val[tot] = x;
        fa[tot] = f;
        if (f)
            son[f][x > cal[f]] = i;
    }

    up(i);
    up(fa[i]);

    if (!rt)
        rt = i;
    splay(i);

    return;
}

inline void erase(int x)
{
    int i = rt;
    while (i and val[i] != x)
        i = son[i][x > val[i]];

    if (!i)
        return;
    splay(i);

    if (!(--cnt[i]))
    {
        if (!ls(i) and !rs(i))
            clear(i), rt = 0;
        else if (!ls(i))
            rt = rs(i), fa[rt] = 0, clear(i);
        else if (!rs(i))
            rt = ls(i), fa[rt] = 0, clear(i);
        else
        {
            int p = rt_pre();
            rs(p) = rs(i);
            fa[rs(i)] = p;
            clear(i);
        }
    }

    up(rt);
}

inline int rk(int x)
{
    int res = 0, i = rt;
    while (i and val[i] != x)
    {
        if (x < val[i])
            i = ls(i);
        else
            res += siz[ls(i)] + cnt[i], i = rs(i);
    }

    res += siz[ls(i)];
    splay(i);
    return res + 1;
}

inline int kth(int k)
{
    int i = rt;
    while (i)
    {
        int lcnt = siz[ls(i)];
        if (lcnt < k and k <= lcnt + cnt[i])
            break;
        else if (k <= lcnt)
            i = ls(i);
        else
            k -= lcnt + cnt[i], i = rs(i);
    }

    splay(i);
    return val[i];
}

inline int getpre(int x)
{
    int res = (insert(x), rt_pre());
    erase(x);
    return val[res];
}

inline int getnxt(int x)
{
    int res = (insert(x), rt_nxt());
    erase(x);
    return val[res];
}

Treap

树与堆的结合。

利用了堆的性质保证了树结构的平衡,即层数达到期望 \(log\) 。

Treap的每个节点需要一个额外的权值作为优先级,去维护它作为堆的性质。因此整棵树不仅要符合BST的特性,也要满足父节点的额外权值大于两个子节点。

我们一般设此权值为 \(ord\) ,且是随机生成,因此Treap是一种弱平衡的结构。

Treap有两种实现形式,我们一一讲解。

带旋Treap

是Treap的最初形式,用“左旋”与“右旋”去维护平衡,但它不支持区间操作。

我们去定义一些存储结构。

#define ls(x) son[x][0]
#define rs(x) son[x][1]
const int Maxn = 1e5 + 10;
int fa[Maxn], son[Maxn][2];
int val[Maxn], cnt[Maxn];
int siz[Maxn];
int ord[Maxn];

带旋的Treap常数较小且不容易卡,因为我们会对每一个节点去随机出来一个值作为堆的优先级,同时在每次删除或者插入时根据这个权值去决定旋转与否。

旋转:

我们需要在不影响树的性质的前提之下,把和旋转方向相反的子树变为根,把原来根结点作为与旋转方向相同的子节点,且左旋与右旋操作是相互的。

那么就根据堆的性质往上转即可,因为是小根堆,那么上面的优先级一定会更小。最终也就是让左子节点或者右子节点变为根结点即可。

        A                 C
       / \               / \
      B   C    ---->    A   E
         / \           / \
        D   E         B   D

inline void up(int x)
{
    return siz[x]=siz[ls(x)]+siz[rs(x)]+1,void();
}

inline void spin(int &i,int p)
{
    int t=s[i][p];
    s[i][p]=s[t][!p];
    s[t][!p]=i;
    up(i); up(t);
    i=t;

    return;
}

插入和普通的BST没啥太大区别,只需要注意要通过旋转来维护堆的性质。至于删除的话就大力分类讨论即可,考虑删除后谁适合当父节点,且要注意更改树的大小。

然后我们就可以再次利用构建的性质去进行操作。

注意从根结点开始递归。

inline void ins(int x, int &i)
{
    if (!i)
    {
        i = ++tot;
        siz[i] = 1;
        val[i] = x, pri[i] = rand();
        return;
    }

    siz[i]++;
    if (x <= val[i])
    {
        ins(x, ls(i));
        if (pri[ls(i)] < pri[i])
            spin(i, 0);
    }
    else
    {
        ins(x, rs(i));
        if (pri[rs(i)] < pri[i])
            spin(i, 1);
    }

    return;
}

inline void del(int x, int &i)
{
    if (x == val[i])
    {
        if (ls(i) * rs(i) == 0)
        {
            i = son[i][0] + son[i][1];
            return;
        }
        if (pri[ls(i)] > pri[rs(i)])
        {
            spin(i, 1);
            del(x, ls(i));
        }
        else
        {
            spin(i, 0);
            del(x, rs(i));
        }
    }
    else if (val[i] > x)
        del(x, ls(i));
    else
        del(x, rs(i));

    up(i);

    return;
}

inline int rk(int x, int i)
{
    if (!i)
        return 1;
    if (val[i] >= x)
        return rk(x, ls(i));
    return rk(x, rs(i)) + siz[ls(i)] + 1;
}

inline int kth(int x, int i)
{
    if (siz[ls(i)] == x - 1)
        return w[i];
    if (siz[ls(i)] >= x)
        return ask(x, ls(i));

    return kth(x - siz[ls(i)] - 1, rs(i));
}

inline int pre(int x, int i)
{
    if (!i)
        return -inf;
    if (val[i] < x)
        return max(val[i], pre(x, rs(i)));
    else
        return pre(x, ls(i));
}

inline int nxt(int x, int i)
{
    if (!i)
        return inf;
    if (val[i] > x)
        return min(val[i], nxt(x, ls(i)));
    else
        return nxt(x, rs(i));
}

鸽了

标签:return,rs,int,siz,fa,ls,数据结构
来源: https://www.cnblogs.com/ztemily/p/16513844.html