• 「笔记」左偏树


    写在前面

    左偏树是在二叉树的结构上进行维护的,在这个二叉树中它满足堆的性质。

    特殊的是,左偏树可以在 (O(log_2 n)) 的时间内进行合并操作。

    下面的讲解默认左偏树为小根堆

    正文

    一些定义

    • 外结点:左儿子或右儿子为空的结点。
    • 距离 (dis_i) : 表示结点 (i) 到 外结点的最短距离。特别的空结点的距离为 (-1)
    • (lson/rson):左右儿子、
    • (val_i):结点 (i) 的权值。

    基本性质

    堆性质:对于每个节点 (x) ,满足 (val_x < val_{lson_x}, val_x < val_{rson_x})。注意这里 (val_{lson_x})(val_{rson_x}) 的大小关系并不能确定。
    左偏性质:对于每个节点 (x) ,有 (dis_{lson_x} > dis_{rson_x})

    当然反过来就可以叫右偏树了

    几个结论

    • 结点 (x) 的距离 (dis_x = dis_{rson_x} + 1)

    • 距离为 (n) 的左偏树至少有 (2^{n+1}-1) 个结点,此时它的形态是一个满二叉树。

    • (n) 个结点的左偏树树高为 (log_2 n),可由第二个结论推导而来。

    核心操作-合并操作

    定义 Merge(x,y) 为合并两个分别以 (x,y)为根的左偏树,返回值为新根。

    首先不考虑左偏树的性质,考虑合并两个具有堆性质的二叉树。默认为小根堆。

    1、如果 (val_x < val_y) ,根节点为 (x),否则为 (y),为了避免分类讨论可以将 (x,y) 交换。

    2、将 (y)(x) 的一个儿子合并,用合并后的根节点代替 (x) 的这个儿子的位置。

    3、递归的进行上述过程,如果 (x)(y) 为空节点,返回 (x+y)

    设树高为 (h) ,每次合并 (h_x + h_y) 都会减少 (1) ,所以复杂度是 (O(h)) 的,知道刻意造一下数据,使其合并后退化为一条链,可以把每次合并卡成 (O(n)) 的。

    这显然不是我们想要的,考虑怎么合并让他变得更加平衡?

    利用 (FHQ-Treap) 的思想每次随机选择一个结点合并? 应该是可以的。

    但是我们左偏树的性质还没用啊。

    因为左偏树中 左儿子的距离大于右儿子的距离,这说明右子树结点数更小,所以我们 每次将 (y)(x) 的右儿子合并

    最后总的树高 (h = log_2 n),每次合并的复杂度为 (O(log_2 n))

    注意一次合并完可能不在满足左偏树的性质。这时候我们把左右儿子交换就好了。

    至于 (dis_x),显然初始化时都是 (0),合并完根据上面的第一个推论更新 (dis_x) 的值即可。

    下面结合代码理解。

    基操-插入一个新的结点

    新建一个结点然后执行 Merge 操作即可

    基操-找一个结点的根节点

    不断跳 (fa) 即可。

    可能会太慢。

    路径压缩!

    具体原理和并查集相同。

    基操-求最小值

    默认小跟堆,返回根节点对应权值即可。
    大根堆同理。

    基操-删除一个最小值

    把根节点架空,合并两个子树即可。

    注意路径压缩带来的影响,所以合并的时候要让三者的 (fa) 都指向新的根节点。

    同时注意标记已删除的节点,清除已删除的节点的信息。

    假设根节点为 (x)

    fa[lson[x]] = fa[rson[x]] = fa[x] = Merge(lson[x], rson[x]);
    vis[x] = true; // 标记已被删除
    lson[x] = rson[x] = dis[x] = 0; // 清除信息
    

    Code

    namespace LIT {
        #define ls lson[x]
        #define rs rson[x]
        int lson[MAXN], rson[MAXN], fa[MAXN], dis[MAXN];
        bool vis[MAXN];
        struct node { // 如果题目没有要求直接开 int 也可以。
            int pos, val;
            bool operator < (const node &b) { 
                return val == b.val ? pos < b.pos : val < b.val; 
            }
        }val[MAXN];
        int Find(int x) { return fa[x] == x ? x : fa[x] = Find(fa[x]); } // 路径压缩
        int Merge(int x, int y) {
            if(!x || !y) return x + y;
            if(val[y] < val[x]) swap(x, y);
            rs = Merge(rs, y);
            if(dis[ls] < dis[rs]) swap(ls, rs);
            dis[x] = dis[rs] + 1;
            return x;
        }
    }
    

    例题

    P3377 【模板】左偏树(可并堆)

    题目传送

    模板题。

    P2713 罗马游戏

    题目传送

    模板题。

    P1456 Monkey King

    题目传送

    简述题意:一开始有 (n) 个猴子,猴子有强壮值,进行 (m) 次合并,合并后两个猴子属于一个群体。每次合并给你两个猴子编号,需要先将这两个猴子所在群体中的猴王(最强的)的强壮值减半,然后进行合并。

    Solution

    利用左偏树的性质进行模拟即可。

    对于每个群体,我们可以先把它的猴王删掉,然后更改猴王的强壮值,再把它合并进来。

    然后直接合并两个群体就做完了。

    修改猴王的强壮值要直接在它对应的结点修改。

    如果新建结点的话会出现一些奇怪的错误,目前不清楚,可以看一下KnightL 的帖子
    大概是因为关系紊乱?毕竟猴王只是不那么强壮了而不是挂掉了,也不可能是生出一个小猴王。

    Code

    /*
    Work by: Suzt_ilymics
    Problem: 不知名屑题
    Knowledge: 垃圾算法
    Time: O(能过)
    */
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    #include<queue>
    #define LL long long
    #define orz cout<<"lkp AK IOI!"<<endl
    
    using namespace std;
    const int MAXN = 1e5+5;
    const int INF = 1e9+7;
    const int mod = 1e9+7;
    
    int n, m;
    
    int read(){
        int s = 0, f = 0;
        char ch = getchar();
        while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
        while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
        return f ? -s : s;
    }
    
    namespace LIT {
        #define ls lson[x]
        #define rs rson[x]
        int fa[MAXN], lson[MAXN], rson[MAXN], dis[MAXN];
        struct node {
            int pos, val;
            bool operator < (const node &b) {
                return val == b.val ? pos > b.pos : val > b.val;
            }
        }val[MAXN];
        int Find(int x) { return fa[x] == x ? x : Find(fa[x]); }
        void Clear() {
            memset(lson, false, sizeof lson);
            memset(rson, false, sizeof rson);
            memset(dis, false, sizeof dis);
            memset(fa, false, sizeof fa);
        }
        int Merge(int x, int y) {
            if(!x || !y) return x + y;
            if(val[y] < val[x]) swap(x, y);
            rs = Merge(rs, y);
            if(dis[ls] < dis[rs]) swap(ls, rs);
            dis[x] = dis[rs] + 1;
            return x;
        }
    }
    using namespace LIT;
    
    int main()
    {
        while(cin >> n) {
            Clear();
            for(int i = 1; i <= n; ++i) fa[i] = val[i].pos = i, val[i].val = read();
            m = read();
            for(int i = 1, u, v; i <= m; ++i) {
                u = read(), v = read();
                int uf = Find(u), vf = Find(v);
                if(uf == vf) puts("-1"); 
                else {
                    int x = fa[lson[uf]] = fa[rson[uf]] = fa[uf] = Merge(lson[uf], rson[uf]);
                    int y = fa[lson[vf]] = fa[rson[vf]] = fa[vf] = Merge(lson[vf], rson[vf]);
                    lson[uf] = rson[uf] = dis[uf] = 0;
                    lson[vf] = rson[vf] = dis[vf] = 0;
                    val[uf].val /= 2, val[vf].val /= 2;
                    fa[uf] = uf, fa[vf] = vf;
                    x = fa[x] = fa[uf] = Merge(x, uf);
                    y = fa[y] = fa[vf] = Merge(y, vf);
                    fa[x] = fa[y] = Merge(x, y);
                    printf("%d
    ", val[fa[x]].val);
                }
            }
        }
        return 0;
    }
    
    

    鸣谢

    左偏树-KnightL
    题解 P3377 【模板】左偏树(可并堆)- hsfzLZH1

  • 相关阅读:
    HDU
    01字典树模板
    扩展欧几里得和乘法逆元
    HDOJ-1156 Brownie Points II 线段树/树状数组(模板)
    CF-825E Minimal Labels 反向拓扑排序
    CF-831D Office Keys 思维题
    RMQ 解决区间查询问题
    hdu 5073 有坑+方差贪心
    hdu 5074 相邻数和最大dp
    hdu 5078 水题
  • 原文地址:https://www.cnblogs.com/Silymtics/p/14942000.html
Copyright © 2020-2023  润新知