所谓可并堆,就是可以合并的堆。
本文介绍配对堆和左偏树。
未完工。
什么是可并堆
什么是堆我就不解释了。
上面说了,可并堆就是可以合并的堆。
最基础的操作有:快速查询最大/最小值、插入一个值、删除最大/最小值、合并两个堆。
如果用二叉堆的话,可以在 $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));
}
不太好形容,画了个图:
需要注意的是:该递归实现已经满足了顺序要求,如果要修改成非递归,要注意顺序。
于是删除实际上非常简单:
Node *pop() { return root = merges(root->son); }
删除给定结点
注:下文我没有进行过测试,是我口胡的产物。
首先对一个点记录他的前一个结点 father(不是树形结构的父亲),然后操作都需要稍微修改一下来维护这个东西。
接着把这个点对应的子树分离出来,然后执行 pop 操作,最后和原树合并一下。
不怎么好实现,不建议,而且时间复杂度感觉并不是很好。
左偏树
待填。