所谓可并堆,就是可以合并的堆。

本文介绍配对堆和左偏树。

未完工。

什么是可并堆

什么是堆我就不解释了。

上面说了,可并堆就是可以合并的堆。

最基础的操作有:快速查询最大/最小值、插入一个值、删除最大/最小值、合并两个堆。

如果用二叉堆的话,可以在 $O(\log^2n)$​​ 的复杂度内启发式合并堆,但下文说的两个数据结构可以更快地实现。

配对堆

配对堆,英文 pairing heap。配对堆,顾名思义就是两两配对的堆。

其复杂度都是势能分析后的均摊的结果,所以不能可持久化

声明:这个数据结构的复杂度我不会证明。

定义

配对堆是一个多叉树,每个点有一个权值,满足每个点的权值都小于其所以儿子的权值。(注:本文只考虑小根堆)

存储的时候使用孩子兄弟表示法,即对每个点记录他的最左面的儿子,以及他右面的兄弟。

struct Node
{
    int val;
    Node *son, *bro;
};

查询最小值

返回根的权值即可。$O(1)$​。

合并

把根权值较大的堆变为另一个堆的一个儿子即可。(画个图就知道代码非常显然了)

Node *merge(Node *a, Node *b) // 传入两个堆的根,返回合并后堆的根
{
    if (a == nullptr) // 如果有一个是空节点,直接返回另一个
        return b;
    if (b == nullptr)
        return a;
    if (a->val > b->val) // 保证 a 是合并后堆的根
        swap(a, b);
    b->bro = a->son; // a 儿子要变为 b 的兄弟
    a->son = b; // a 儿子变为 b
    return a;
}

单次显然是 $O(1)$,势能分析后仍然是 $O(1)$。

插入

把这个数看成一个堆然后和原堆合并即可。

删除最小值(重点)

考虑删除之后,我们需要把一个森林合并成一个树。

我们显然可以把儿子任意一个一个合并,但会导致复杂度退化到 $O(n)$​。

于是有一个很乱搞的优化:将两个堆从左往右两两合并,再从右往左依次合并,复杂度会均摊为 $O(\log n)$。

搞一个函数来合并一个点的所有兄弟:

Node *merges(Node *x) // 输入根,返回合并后的根
{
    if (x == nullptr || x->bro == nullptr)
        return x;
    Node *a = x->bro, *b = a->bro;
    x->bro = a->bro = nullptr; // 拆散
    return merge(merge(x, a), merges(b));
}

不太好形容,画了个图:

oBIP0g.jpg

需要注意的是:该递归实现已经满足了顺序要求,如果要修改成非递归,要注意顺序。

于是删除实际上非常简单:

Node *pop() { return root = merges(root->son); }

删除给定结点

注:下文我没有进行过测试,是我口胡的产物。

首先对一个点记录他的前一个结点 father(不是树形结构的父亲),然后操作都需要稍微修改一下来维护这个东西。

接着把这个点对应的子树分离出来,然后执行 pop 操作,最后和原树合并一下。

不怎么好实现,不建议,而且时间复杂度感觉并不是很好。

左偏树

待填。