Lazy Binomial Heaps

motivation

为什么我们需要Binomial heaps(二项堆)? Binary heaps(二叉堆) 实现的优先队列就已经有了 O(logn) 复杂度的 enqueue(insert) 和 extractMin(deleteMin),findMin O(1), 实际运作过程中就已经很快了,为何还需要其他 堆 呢?

因为很多图算法都不止依赖于优先队列的这两种操作,还依赖于它提供的额外操作: meld (merge or union)——合并两个优先队列 (MSTs via Cheriton-Tarjan)decreaseKey——提高队列内某个元素的priority (Shortest paths via Dijkstra)add_to_all——add $\Delta $k to the keys of each element in pq.

Meldable Priority Queue

试想如何用二叉堆实现的优先队列实现两个 pq 的合并呢?可以两个混在一块 heapify,O(n) 看起来还可以;或是调用接口 A.insert(B.deleteMin()),O(nlogn) 看起来就不太行。

因为二叉堆是完全二叉树,所以很难简单地把两个二叉堆通过 link 合并成新的二叉堆。

how to meld

而如果我们用二进制来表示这两个 pq,那么合并相当于 二进制加法,这样复杂度就变成了 O(logn+logm)。在这种思路下,我们把 n 和 m 表示成两个 packets collection每一个 packet 2 的幂次大小,用来存数。然后用指针把这些 packet 和 node link,这样合并就变成了:

intuition

meld

更多细节——Recap from last time

我们在算法设计搬运(3)——Heaps这部分内容里讲了 binomial treebinomial heaps 的操作及性能分析。因为我们在这里已经不是在搬运 570 的内容了,而是在补充 Victor 在 570 上略过的知识,为它们做详细补充,更详细的的披露细节,首先来是 binomial tree 的性质。

Properties of Bk

  • 含有 2k 个 nodes
  • height 为 k
  • i i i层有 ( i k ) (_i^k) (ik)个 nodes
  • rank(root(Bk)) = rank(Bk) = k
  • 根的孩子从左到右为 Bk-1, Bk-2,…, B0.

而 binomial heap H 则是 binomial tree 的 set,且 set 中的 B 满足 min-heap 的性质。对于 eager binomial heap,我们还要求 B 的 rank 唯一。

缺失的细节是 order 和 min 指针

然后之前没有涉及到 meld 操作,那么对于 eager binomial heap,meld(H1,H2): H2 order 低到高往 H1 里添加,扫描对应位置,rank 不唯一则比较根大小,link,order+1,直至 rank 唯一,直到全部添加完,O(log n)。另外,insert(H,v) 的实现实际上就是 MAKE-HEAP(v), 在对这个只含单一节点的 heap 和 H 进行 meld。

其他操作和复杂度分析详见算法设计搬运(3)——HeapsPotential Method —— amortized analysis

lazy binomial heap

where we stand

在继续下面的内容之前我们先看一下 eager binomial heap 的性能表现。
performance of eager binomia heap

(MINIMUM 因为存了指针,在 insert 和 extractMin 时顺便 update,所以 Θ ( 1 ) \Theta(1) Θ(1).)

根据“对于 comparison-based sort 需要 Ω ( n log ⁡ n ) \Omega(n\log n) Ω(nlogn)”这个定理,看看我们现在的处境可以发现,能优化的地方不多。我们不可能让 insert 和 extractMin 都优化到更快——指都小于O(logn),但是却可以考虑使其中一项操作变得更快。

那么就考虑优化 insert 吧!看看能不能使在 worst-case 也可以 O ( 1 ) O(1) O(1),毕竟删前总要插。那么因为 insert 或 enqueue 是用 meld 实现的,那么 meld 也必须 O ( 1 ) O(1) O(1)了。

问题:为什么我们在插入时就要维护呢,我们都不一定要对新插入进来的节点做任何操作?

思路:我们索性 insert 或 meld 的时候就只维护 min 指针,然后直接把 H2 直接连进来,复杂度 O ( 1 ) O(1) O(1),然后如果有其他要维护的事情就全交给 extractMin 吧!

例: 对于 lazy binomial heap 插入 4, 7, 3, 6, 5. (In practice,实现是双向循环链表)
lazy meld

试想一下这样实现 lazy meld 之后,extractMin 会怎样?

  • 先 remove min 指针所指向 binomial tree 的 root,把 Bk 分裂成 BK-1…B0 再 lazy meld 回来
  • 再 update min 指针

可达鸭眉头一皱,发现在最后 update 时的复杂度取决于 此时 heap 里面 tree 的个数,O(t)。而t = O(n),于是 extractMin O(n)。

像极了中学假期在家的你。白天父母外出工作,于是玩的时候不需要考虑收拾可以随意地造。其余的事情就等着之后掐着父母下班的点再把一切收拾干净,装作这一天平静度过什么都没有发生一样。恭喜你年纪轻轻就已经是个优化带师,成功地把 van 的复杂度降到了最低。

接着来解决 extractMin 变 O(n) 的问题。分析可知这是由于我们不再限制 Bk 的order唯一,meld 时只单纯 link。那么就考虑我们对应 lazy meld,在 extractMin 时把该合并的合并了,保证 order 唯一这样我们的树的个数就变 O(log n)。

这个在 extractMin 时重新合并 tree 使 order 再次唯一的操作在某些教材上称之为 coalesce step.

coalesce step

简言之, coalesce step 就是在extractMin之后,update min 指针之前合并所有order相同的 tree 使 order 唯一。

考虑到同 order 合并,且 order 破碎,使用基排序来高效合并。

coalesce step

先创建 O(logn)个箱子,O(logn)。再 distribute 按 order 所有的 tree,O(t)。再合并所有需要合并的 tree,O(t)。总复杂度 O(t+logn)。

所以此时 extractMin 的实现:

  • 先 remove min 指针所指向 binomial tree 的 root,把 Bk 分裂成 BK-1…B0 再 lazy meld 回来, O(logn)
  • 再 coalesce step, O(t+logn)
  • 最后 update min 指针, O(logn)

很遗憾,extractMin worst-case 复杂度 O(t+logn) = O(n)。heap 里全是 B0. 而 insert, meld, minimum O(1). 即便我们此时还不考虑 decreaseKey,但依旧是O(logn),取决于树的高度。

extractMin

The story so far

lazy binomial heap 的 worst-case 性能似乎看起来并不乐观。实际上我们是以牺牲了extractMin 的性能为代价换取了 meld 的高效。那么这个数据结构的均摊复杂度如何呢?

继续使用上次扩充的 potential method.

首先 meldinsert worst-case Θ ( 1 ) \Theta(1) Θ(1), amortized 同样 Θ ( 1 ) \Theta(1) Θ(1)

Minimum 存了指针,所以依旧 Θ ( 1 ) \Theta(1) Θ(1)

最后是关于extractMin的 amortized analysis. 依旧令 Φ ( S ) \Phi(S) Φ(S)表示当前 heap 里 tree 的数量。假设 extractMin 前 Φ ( S b e f o r e ) = t \Phi(S_{before}) = t Φ(Sbefore)=t
actual cost of step 1 = O ( log ⁡ n ) = O(\log n) =O(logn)
actual cost of step 2 = O ( t + log ⁡ n ) = O(t+\log n) =O(t+logn)
actual cost of step 3 = O ( log ⁡ n ) = O(\log n) =O(logn)(前面都提到过)
Φ ( S a f t e r ) = log ⁡ n \Phi(S_{after}) = \log n Φ(Safter)=logn

Δ Φ = − t + log ⁡ n \Delta\Phi = -t + \log n ΔΦ=t+logn, actual cost = O ( t + log ⁡ n ) = O(t+\log n) =O(t+logn).
因此“均摊”复杂度 = O ( t + log ⁡ n ) + C ⋅ ( − t + log ⁡ n ) = O ( log ⁡ n ) O(t+\log n) + C\cdot (-t+\log n) = O(\log n) O(t+logn)+C(t+logn)=O(logn)

最终 lazy binomial heap 的性能如下:
performance of lazy binomial heap
(两个性能的图均来自 Stony Brook CSE-548 lecture9)

后记

主要参考了大S的 cs-166,Stony Brook的 CSE-548

前者会讲很多思路,好像旨在告诉你提出解决办法的人是他们如何思考这个问题的,把你带入到情境中。但是有时候会显得很冗长,啰嗦。而实际上虽然内容很长,200+的slides,但其实并没有披露特别多的细节。而且因为屏蔽了一些细节,所以在一些时候分析的时候就会跟屏蔽细节的结论相悖。

后者很扎实、很干。上来就硬货怼脸的感觉,而且会涉及到比较多的细节,很详尽。可能因为是CSE的缘故…总之如果抱着致知的态度,刨根问底一探究竟的话,极为推荐。可能大S的人都是那种知道顶层思路,享受自己回去自己研究实现的乐趣。确实本来就没有一个正确答案,只要思路对,能实现,细节自己慢慢扣是极好的。

啊…如果本着膜一下名校,去看下CMU的课件,我觉得还是算了吧。看了也白看,内容很少,看了也白看,而且写到 3. Binomial heaps 后面就跟了一句话,To be finished. 在后面就是下一个 lecture的另外的内容了…

最后记一下这些 slides 中的小错。cs-166: performance score find_min Θ ( 1 ) \Theta(1) Θ(1),而在分析时采用的是不维护 min 指针而是去find_min O(t),感觉没得洗。另外就是太长了…不利于阅读…CSE-548 对于lazy binomial heap 是否维护 min 指针的描述前后矛盾。维护顺带的事儿,不维护的话,O(t)啊!?另外可能会不一致在一些操作的先后顺序是先合并还是先remove没有影响。

不维护
使用
create

1、抽象数据类型定义如下: 本程序主要定义了两个类,一个用于创建节点,另一个用于创建二项。 class BinomialHeapNode { public: int key; //节点的关键字的值 int degree; //节点的度 BinomialHeapNode* sibling; //节点的右兄弟节点 BinomialHeapNode* child; //节点的最最孩子节点 BinomialHeapNode* father; //节点的双亲节点 BinomialHeapNode(int newkey) ; //构造函数,以关键字创建节点 BinomialHeapNode(); //重载构造函数,默认关键字为0 }; class BinomialHeap { public: typedef BinomialHeapNode node_t; private: static BinomialHeap heapMerge(BinomialHeap& heapA, BinomialHeap& heapB) ; //将两个二项按照根的度的递增顺序构成一个新的二项 static void nodeLink(node_t* child, node_t* father) ; //将两个节点连接成二项树 public: node_t* head; //二项的头节点 BinomialHeap(); //构造函数,头结点为0 { head = 0 } BinomialHeap(node_t& node) //构造函数,以一个节点进行构造 { head = &node; } BinomialHeap(node_t* node) //构造函数,以一个节点进行构造 { head = node; } BinomialHeap(const BinomialHeap& src) //构造函数,以一个二项进行构造 { head = src.head; } inline void insertNode(node_t& node) ; //在中插入一个节点 void insertNode(node_t* node) ; //在中插入一个节点 node_t* Find_Min(); //查找关键字值最小的节点 node_t* Extract_Min(); //查找并删除关键字值最小的节点 static BinomialHeap heapUnion(BinomialHeap& heapA, BinomialHeap& heapB) ; //合并两个二项 node_t*Findnode(int key) ; //查找关键字为key的节点 inline void swapkey(node_t* source,node_t*target ) ; //交换两个节点的关键字的值 void decreaseKey(int key,int newkey) ; //减小关键字为key的节点的值为newkey void traverse(); //遍历二项 void DeleteNode(int key) ; //删除关键字为key的节点 };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值