左偏树

左偏树是一种可并堆(一系列的堆),支持以下操作:

  1. 删除一个堆的最值。

  2. 查询一个堆的最值。

  3. 新建一个堆,只包含一个元素。

  4. 合并两个堆。这个复杂度是 \(O(\log)\) 的。

左偏树是一颗二叉树。定义 “外结点” 为儿子数量不等于 \(2\) 的结点,定义每个结点的 \(dist\) 为该结点到最近的外结点的距离。

左偏树满足对于每个节点,左儿子的 \(dist>\) 右儿子的 \(dist\)

左偏树满足堆的性质,即一个结点是它子树内的最值。

对于 2 操作,输出一个左偏树的根即可。

对于 3 操作,显然很简单。

如果我们实现了 4,则 1 就只需要合并根的左右儿子,然后删除根即可。

如何实现 4 ?我们观察到一个性质。如果左偏树从根出发一直向右走,最多走 \(\log n\) 步,就会到达一个没有子结点的结点 \(x\)

证明:假设还能继续往下走。注意到 \(x\) 已经是最右的结点了,所以为了使它的祖先们满足左 \(dist>\)\(dist\),如果截取深度为 \(1\sim dth[x]\) 的这一段,必构成满二叉树。满二叉树结点个数是 \(2\) 的幂,所以 \(d[x]\le \log n\)

根据这个结论,可以设计出 \(O(\log n)\) 的合并堆算法:

  1. 初始有两颗左偏树 \(h1,h2\),不妨 \(h1\) 的根优于 \(h2\)

  2. 合并 \(h1\) 右节点的子树 和 \(h2\),将合并出来的左偏树作为 \(h1\) 的右儿子。

  3. 如果此时 \(h1\) 右儿子的 \(dist\) 大于左儿子,交换左右儿子。

使用并查集快速查询某个结点所在堆的根。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;

int n, m;
// 值类型,默认构造函数应保存最不优先的值
struct Val {
    int v;
    Val() {
        v = inf;
    }
    Val(int x) {
        v = x;
    }
} ide;

// 定义为大的优先
bool operator<(Val x, Val y) {
    return x.v > y.v;
}

struct LefHeap {
    int sz;
    vector<Val> a;          // 值
    vector<bool> f;         // 是否未删除
    vector<int> l, r, d, p; // 左子、右子、深度、并查集父结点
    // 查找x号元素所在的根
    int fnd(int x) {
        if (p[x] == x)
            return x;
        return p[x] = fnd(p[x]);
    }
    // 合并x, y号元素为根的堆,返回根编号
    int unn(int x, int y) {
        if (x == 0 || y == 0) // 一方为空,另一方有效
            return x + y;
        if (a[x] < a[y])      // 否则,x为根
            swap(x, y);
        p[y] = x, r[x] = unn(r[x], y);
        if (d[l[x]] < d[r[x]])
            swap(l[x], r[x]);
        d[x] = d[r[x]] + 1; //更新dist 
        return x;
    }
    // 加入新元素,值为x
    void push(Val x) {
        sz++;
        l.push_back(0), r.push_back(0), d.push_back(0);
        p.push_back(sz), a.push_back(x), f.push_back(true);
    }
    // 查找x号元素所在堆的顶
    Val top(int x) {
        return a[fnd(x)];
    }
    // 删除x号元素所在堆的顶
    void pop(int x) {
        int rt = fnd(x), new_rt = unn(l[rt], r[rt]);
        f[rt] = 0, p[rt] = p[new_rt] = new_rt; // 并查集换根
    }
    LefHeap() {
        sz = 0;
        p = l = r = d = vector<int>(1, 0);
        a = vector<Val>(1, ide);
        f = vector<bool>(1, false);
    }
} h;

int main() 
{
    cin >> n >> m;
    for (int i = 1, t; i <= n; i++) {
        cin >> t;
        h.push(t);
    }
    for (int i = 1, op, x, y; i <= m; i++) {
        cin >> op;
        if (op == 1) {
            cin >> x >> y;
            if (h.f[x] && h.f[y] && h.fnd(x) != h.fnd(y))
                h.unn(h.fnd(x), h.fnd(y));
        }
        else {
            cin >> x;
            if (h.f[x]) {
                cout << h.a[h.fnd(x)].v << endl;
                h.pop(x);
            }
            else
                cout << -1 << endl;
        } 
    }
    return 0;
}

:为什么删除的时候还要让 p[rt] = new_rt

因为此时我们想要原本所有指向 rt 的都指向 new_rt,直接让 rt -> new_rt 即可。

注2:只能写 rt -> new_rt,不能写 l[rt],r[rt] -> new_rt,因为路径压缩时有的已经直接指向 rt 了。

posted @ 2024-02-24 17:16  FLY_lai  阅读(4)  评论(0编辑  收藏  举报