速通平衡树
基础
前言:本文是我对平衡树的总结,只是平衡树的框架,希望可以帮助读者快速理解平衡树的思想,如果需要平衡树的具体实现,请点这里:无旋平衡树之 fhq treap(图解)❤️ | 春水煎茶
我重学了一边 FHQ Treap,感觉这次才真正地把FHQ搞明白,现在觉得平衡树是一种相当直观的数据结构。
平衡树底层逻辑,就是要维护一种数据结构,使得所有节点的左儿子中的所有节点的权值小于该节点权值小于所有右儿子中的所有节点的权值。
满足这个性质的树有很多个,我们肯定希望在满足性质的时候树的深度不能太深。这只需要数据随机就行了。可以给每个节点随机赋值一个 rnd 权,然后让 rnd 权满足堆的性质即可。容易得到,树的深度接近log(所以平衡树的复杂度是\(O(n\log n)\))
如果既维护平衡树的性质,树的深度又不深,我们就可以用类似二分的去找前驱,后继。如果记录了子树的大小,还可以根据排名找点(还是二分),根据点找排名(在树上找到该点,然后确定在其左边的数的个数)
我目前会两种方法既维护平衡树的性质,又维护堆的性质。第一种是用旋转,zag和zig,在算法竞赛进阶指南上有详细的介绍;第二种是通过分裂合并的时候来维护,也就是大名鼎鼎的FHQ无旋Treap,这个的通用性更强。
FHQ有分两种:
第一种是以每个点的点权作为树上的点权并维护树的性质,优点是可以查前驱后继排名之类的东西,缺点是原来数列的形态被破坏了,不能在原数列上区间操作。不过这种FHQ被 pb_ds 的 rb_tree 全方面超越了,一般用不着自己手写,但是学了这个可以更好地理解第二种FHQ。pbds使用教程.
第二种是以每个点的下标为树上的点权,优点维护了原来数列的形态,可以在原数列上区间操作,而且其对区间翻转的优势几乎不具有替代性。缺点不能查前驱后继排名之类的东西。维护方法就是像线段树一样打标记。
如果想要同时区间加减和维护排名,可以使用分块:每一个块内要维护该块内排序后的数组(散块暴力重构即可,用双指针排序是线性的),询问时,整块使用lower_bound查排名。实在不行就每个块内维护一个第一类FHQ(pb_ds 的 rb_tree)。
但不管是那种FHQ,其核心操作还是分裂与合并。
void split(int p, int k, int &x, int &y) {
    if(!p) {x = y = 0; return ;}
    pd(p);
    if(k <= lsz) {
        y = p;
        split(ls, k, x, ls);
    } else {
        x = p;
        split(rs, k - lsz - 1, rs, y);
    }
    upd(p); 
}//按下标分裂 
int merge(int x, int y) {
    if(!x || !y) return x | y;
    if(t[x].rnd < t[y].rnd) {
        pd(x);
        t[x].r = merge(t[x].r, y);
        upd(x);
        return x;
    } else {
        pd(y);
        t[y].l = merge(x, t[y].l);
        upd(y);
        return y;
    }
}//返回根节点编号
拓展
可持久化
肯定是只修改访问目标区间会路过的节点,然后复制一些节点,复制的节点指向的左右儿子与原节点相同。这其实和主席树很像。可持久化平衡树有一点要注意的是,平衡树上的每一个节点都可能作为多个节点的儿子,一般来说要修改的只是它作为其中某个节点的儿子时的信息。所以不能直接修改这个节点,而是要复制一个新的节点来修改。
区间复制
也就是把区间 \([l,r]\) 复制到 \([l2, r2]\),可以直接把 \([l2,r2]\) 对应的节点换成区间 \([l,r]\) 对应的节点,这也会导致一个节点作为多个节点的儿子,所以把修改节点信息改为必须复制一个节点然后修改复制的节点。这与可持久化类似。如果卡空间就定期重构。
有交合并
如果要合并以 x 为根的平衡树 a,和以 y 为根的平衡树 b,那么可以先把 b 树分裂成权值小于等于 x 的部分和权值大于 x 的部分,然后把权值小于等于 x 的部分与 a 左儿子合并,大于 x 的部分与 a 右儿子合并。

                
            
        
浙公网安备 33010602011771号