3

算法学习笔记(21): 平衡树(二) - jeefy

 1 year ago
source link: https://www.cnblogs.com/jeefy/p/17396697.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

平衡树(二)

平衡树(一)链接:算法学习笔记(18): 平衡树(一) - jeefy - 博客园

本文中将讲述一下内容:

  • 可持久化Treap

  • 基于Trie 平衡树(后文称之为 BSTrie

  • BSTrie的可持久化

可持久化Treap

可持久化Treap基于FHQ-Treap。其实不难发现,FHQ-Treap在分裂和合并时在每一层只对一个结点产生影响。于是我们可以大胆的可持久化此结构,且保证复杂度为 O(logn)O(log⁡n)。

202303090946046.png

我们以此图为例。我们只需要复制下被影响的结点,基于原树获得一条新链:

202305130812238.png

红色节点是新产生的节点(可能实际上产生的顺序不一样)

其中 (8, 11) 作为新右树的根,(7, 8) 作为新左树的根

也就是说,我们经过一个结点复制一次即可。

inline void splitVal(int p, int val, int &x, int &y, bool simple = true) {
    if (!p) return (void)(x = y = 0);
    int np = simple ? p : clone(p);
    if (val(p) <= val)
        x = np, splitVal(rp(p), val, rp(x), y, simple), update(x);
    else
        y = np, splitVal(lp(p), val, x, lp(y), simple), update(y);
}

simple就是标志是否需要可持久化……特别简单

其实到这里就已经讲完了可持久化Treap了……因为其他绝大部分操作只需要对于分裂时持久化,在合并时并不需要持久化。

例如删除操作:

inline int insert(int root, int val, bool simple = false) {
    int x(0), y(0), p(++usage);
    nodes[p].init(val);
    splitVal(root, val, x, y, simple);
    return merge(merge(x, p), y);
}

这也请读者自行思考为什么不需要合并时可持久化。

基于Trie的 平衡树(BSTrie)

这里基于的Trie指的是 01Trie01Trie,考虑其实每一个数都可以被拆分为二进制,于是有了此做法。

说实话,代码无比之简短,并且十分迅速,除了空间复杂度较大之外,令我不禁想要抛弃WBLT……

后记:空间复杂度……真的很大,很多普通平衡树能过的空间,Trie真不一定能过

我们首先只考虑正数的情况。如果我们把每一个数都扩展成同一个位长,从高位向低位保存成一棵树。我们从左到右(认为0在左,而1在右)观察其叶节点所对应的值(类似于Leafy Tree),可以知道是单调递增的,于是我们可以轻易的将之进行魔改,从而做到普通平衡树所能做到的事。


template<int N = 100000>
class BSTrie {
private:
    int siz[N << 5];
    int ch[N << 5][2];
    int usage;
    #define newNode() ({ \
        ++usage; \
        siz[usage] = ch[usage][0] = ch[usage][1] = 0; \
        usage; \
    })
}

这是这一颗树需要用到的内容。其实从这里就应该可以知道,其空间复杂度为 O(nlogC)O(nlog⁡C) 其中 CC 表示值域大小,一般为 3232。这与其他空间为 O(n)O(n) 的平衡树相比远远不如……

首先看代码:

void insert(int x) {
    int p = 1; 
    for (int i = BS; ~i; --i) {
        int bt = bit(x, i), &np = ch[p][bt];
        if (!np) np = newNode();
        p = np, ++siz[p];
    }
}

写法有些许迷惑,见谅

其中BS指的是 ⌈logC⌉⌈log⁡C⌉

由于我们需要用到子树的大小以方便 rank,kthrank,kth 操作,所以对于路径上 ++siz[p]

不就是普通的 Trie 插入操作吗?不讲了。

void remove(int x) {
    int p = 1;
    for (int i = BS; ~i; --i) {
        int np = ch[p][bit(x, i)];
        if (!np) return;
        p = np;
    }

    p = 1;
    for (int i = BS; ~i; --i) {
        p = ch[p][bit(x, i)], --siz[p];
    }
}

这里需要注意的是需要两遍向下,以防止 x 并不存在的情况。

与普通的 Trie 删除操作类似,想必代码也十分易懂。

在普通平衡树内查询排名怎么查,这里就怎么查:

  • 进入右子节点,则累加左子树的大小

  • 进入左子节点,则不累加

  • 没有子节点,直接返回当前结果

int rank(int x, bool within = false) {
    int p = 1;
    int rk = 0;
    if (x >= 0) rk += siz[1];
    for (int i = BS; ~i; --i) {
        int bt = bit(x, i), np = ch[p][bt];
        if (bt) rk += siz[ch[p][0]];
        if (!np) return rk;
        p = np;
    }

    return within ? rk + siz[p] : rk;
}

这里对于加入了 within 参数,用于提示是否需要包含 x 出现的次数。

为什么在第8行直接返回是正确的?简单来说就是空子树不会对结果造成影响。具体一点请读者自行思考。

查询第k大

呃,令 x 为当前结果:

  • 若进入左子树,则令 x = x << 1

  • 否则令 x = (x << 1) | 1(打括号是为了方便理解)

如果保证树内存在至少 kk 个数,则一定可以找到正确答案,且不会进入空子树。

但是没有这么多个数……则结果不可预测

int kth(int k) {
    int p = 1;
    int x = 0;
    for (int i = BS; ~i; --i) {
        if (k <= siz[lc(p)]) p = lc(p), x <<= 1;
        else k -= siz[lc(p)], p = rc(p), x = (x << 1) | 1;
    }
    return x;
}

于是,你可以在100行内写出一个优秀的平衡树了……


现在考虑有复数的情况。有两种解决方法:

  • 依据符号位,建两棵树。根据补码的知识,对于有符号类型的整数,其对应的无符号整型数值越大,其值越大。所以负数也可以利用同样的代码处理。

    而改变方法也很简单,将语句 int p = 1 改为 int p = x < 0 ? 1 : 2(标号随意)即可。
    只是在 kth 时,需要有:int x = k <= siz[1] ? (1 << (32 - BS)) - 1 : 0;

    rank 前要多加一句:if (x >= 0) rk += siz[1];

    其他的都没大区别。

  • 第二种方案相对简单,把所有数都加上一个 offset,保证为正整数即可……(这种方法很简答,而且可持久化时也更简单)

于是我们优先采用第二种方法(尤其是需要可持久化的时候)。


可持久化BSTrie

可持久化 Trie 会吧……OK,下课!

还是提一下可持久化的思想:

把每一个经过的结点复制出来即可……类比可持久化Treap的操作


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK