「笔记」左偏树

写在前面

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

特殊的是,左偏树可以在 \(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\) 的值即可。

下面结合代码理解。

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;
    }
}

基操-插入一个新的结点

新建一个结点然后执行 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; // 清除信息

例题

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\n", val[fa[x]].val);
            }
        }
    }
    return 0;
}

鸣谢

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

posted @ 2021-06-27 20:28  Suzt_ilymtics  阅读(138)  评论(0编辑  收藏  举报