学习笔记:左偏树

左偏树是一种可并堆,除了堆的基本功能,最大的特点就是支持合并堆,甚至比普通堆好写。

下面叙述以小根堆为例,大根堆对称。

支持的功能:

  1. \(O(\log n)\) 求一个数所在堆的根
  2. \(O(1)\) 求最小值
  3. \(O(\log n)\) 合并两个堆
  4. \(O(\log n)\) 删除最小值
  5. \(O(\log n)\) 插入一个数

\(n\) 是插入的总节点数(或当前堆的节点数)。

维护的信息:

struct T{
    int l, r, v, d, f;
    // l, r 表示左右儿子, v 表示值
    // d 表示从当前节点到其子树中最近叶子节点的距离 + 1, f 表示当前节点的父亲
} t[N];

基本的结构还是堆,即对于任意节点,它的权值小于等于其子树中任意权值,因此查询最小值只需 \(O(1)\) 访问根即可。

左偏的意义就是:对于任意一个节点的左儿子 \(ls\) 和右儿子 \(rs\),都有 \(t[ls].d \ge t[rs].d\),感性理解就是左子树深的更长。

性质:对于一棵根节点 \(rt\) 满足 \(t[rt].d = k\) 的堆而言,至少有 \(2^k - 1\) 个节点,即一个高度为 \(k\) 的满二叉树的节点树,因为这些节点必不可少,否则 \(d\) 就小于 \(k\) 了。因此对于一个有 \(n\) 个节点的堆,根节点的 \(d\) 就是 \(\log n\) 级别的。

操作:

求一个数所在堆的根

朴素上我们可以一个个跳 \(t[x].f\)。不过我们可以把 \(t[x].f\) 看做一个并查集 \(fa\) 数组,路径压缩一下:

int find(int x) {
    return t[x].f == x ? x : t[x].f = find(t[x].f);
}

这样只要保证我们之后的赋值 \(fa\) 操作都是类似并查集的合并操作,那么复杂度就是 \(O(\log n)\) 的。

求最小值:

找到一个数所在根,直接访问根节点值即可。

合并两个堆

合并 \(merge(x, y)\) 分别以 \(x, y\) 为根的两个小根堆,并返回合并完的根编号:不妨设 \(t[x].v < t[y].v\)(若不满足 \(\text{swap}\) ) ,接着只需递归 \(merge(t[x].r, y)\)。回溯时检查 \(x\) 左右儿子的 \(d\),若不满足左偏树关系交换,返回 \(x\) 即可。

时间复杂度,每次递归,\(x, y\) 之一的 \(d\) 必然减少 \(1\),做多减少到 \(0\),而 \(d\)\(\log n\) 量级的,所以复杂度是 \(O(\log n)\)

貌似网上的复杂度都不是很对,不能每次都赋 \(t[x].fa = x\),这样复杂度就假了,而是函数调用之前把 \(t[y].f = x\),然后内部合并不改变 \(f\) 的值,这样相当于合并两个联通块,复杂度是对的。

int merge(int x, int y) { // 递归合并函数
    if (!x || !y) return x + y;
    if (t[x].v > t[y].v || (t[x].v == t[y].v && x > y)) swap(x, y);
    rs = merge(rs, y);
    if (t[ls].d < t[rs].d) swap(ls, rs);
    t[x].d = t[rs].d + 1;
    return x;
}

int work(int x, int y) { // 合并 x, y 两个堆。
    if (t[x].v > t[y].v || (t[x].v == t[y].v && x > y)) swap(x, y);
    t[x].f = t[y].f = x;
    merge(x, y); return x;
}

删除最小值

找到根节点后,合并两个子树。

void del(int x) {
    t[x].f = work(ls, rs);
}

插入一个数

直接单开一个节点,合并就好了。

posted @ 2020-11-02 19:42  DMoRanSky  阅读(204)  评论(0编辑  收藏  举报