Lec 2Fibonacci 堆与均摊分析
Lec 2 Fibonacci 堆与均摊分析(Fibonacci Heaps & Amortized Analysis)
TL;DR
- 打破优先队列瓶颈:针对图算法中减小键值(
Decrease-Key)远多于提取最小值(Extract-Min)的不对称特征,Fibonacci 堆将减小键值均摊复杂度降低至严格 。 - 惰性延迟合并策略:插入与合并操作仅在根链表中追加单节点,将昂贵的整树合并推迟至提取最小值时批量执行,极大压低常规操作代价。
- 级联剪切保证度数对数界:通过至多丢失一个孩子节点的打标机制与级联剪切,确保度数为 的节点规模以黄金分割比 指数级增长,维持最大度数 。
优先队列与二项堆的局限
优先队列(Priority Queue)是图遍历、网络流与调度系统的核心构件。标准二叉堆(Binary Heap)支持 的插入、删除与减键操作。
| 优先队列结构 | Insert | Find-Min | Extract-Min | Decrease-Key | Union |
|---|---|---|---|---|---|
| 二叉堆 (Binary Heap) | |||||
| 二项堆 (Binomial Heap) | |||||
| Fibonacci 堆 |
注:带 的项目表示均摊复杂度(Amortized Complexity)。
在单源最短路径(Dijkstra)与最小生成树(Prim)算法中,算法总共调用 次 Extract-Min 和至多 次 Decrease-Key。若使用经典堆,总时间为 。由于稀疏图到稠密图中 可达 ,Decrease-Key 的调用频次远超 Extract-Min。如果能将 Decrease-Key 优化至 ,总时间便可直接跃迁为:
在稠密图()中,这一结果直接达到了理论线性的 。Fredman 与 Tarjan(1987)正是为了突破这一理论界限而设计了 Fibonacci 堆。
堆结构设计与惰性哲学
堆的表示:森林与双向循环链表
Fibonacci 堆由一组满足**最小堆序(Min-Heap Property)**的有根树构成的森林组成。每个节点维护以下属性:
key:节点的键值parent:指向父节点的指针child:指向其子节点双向循环链表中某一节点的指针left、right:指向兄弟节点的双向循环指针degree:当前拥有的子节点数量mark:布尔值,指示该节点自挂载到当前父节点后是否失去过子节点
所有树的根节点链接在同一个双向循环链表(根链表,Root List)中。堆对象维护一个指针 min,恒指向当前具有全局最小键值的根节点。
极端惰性设计
二项堆在每次插入时都立即进行类似于二进制加法的进位合并,这使得插入最坏需要 。Fibonacci 堆采取了彻底的**惰性求值(Lazy Evaluation)**思想:
Insert(x):创建一个只包含 的单节点树,将其插入根链表,若 则更新min。完全不做任何合并,耗时 。Union(H1, H2):直接将两个堆的根链表拼接合并为一个环,更新min指针。不触碰任何内部节点,耗时 。
这种“只记账、不结账”的设计积累了大量的未合并单树。真正的整顿工作被推迟到了不可回避的时刻——提取最小值。
提取最小值与合并整理(Consolidation)
执行 Extract-Min(H) 时:
- 移除最小根:将
min指向的根节点从根链表中摘除,将其所有子节点加入到根链表中,释放父节点指针。 - 合并根树(Consolidate):如果此时根链表为空,返回;否则遍历根链表,利用一个度数数组 ,将具有相同度数的根树两两配对合并。
- 比较两根的键值,键值较大者作为子节点挂载到较小者的子节点链表下;
- 较小者的度数加 1;
- 重复此过程,直至根链表中不再存在任何度数相同的两棵树。
- 更新最小值:扫描合并后的所有根节点,重新确定最小根指针
min。
减小键值与级联剪切(Cascading Cut)
当一个节点 的键值减小后,若其新键值小于其父节点 ,最小堆序遭到破坏。为了恢复堆序,必须对树形结构进行调整。
为什么不能随意剪切?
若直接将 剪切下来放到根链表,连续的剪切会导致某个高度极大的节点失去大量子节点,从而变成一条狭长的退化长链。长链的度数极小而节点数极多,这会彻底破坏“度数为 的节点子树规模关于 指数级递增”的数学保证,导致最大度数 退化为 ,令后续 Extract-Min 复杂度恶化为 。
标记位与级联剪切规则
Fibonacci 堆规定:每个非根节点至多只能失去一个孩子节点。 具体规则如下:
- 节点成为根节点时,其标记位重置为
mark = false。 - 当节点 作为非根节点第一次失去某个子节点时,将其标记设置为
mark = true。 - 当已被标记(
mark == true)的节点 失去第二个子节点时,立即触发级联剪切(Cascading Cut):- 将 从其父节点 的子链表中剪切下来,加入到根链表中,并将 重置为
false; - 递归对其原父节点 执行级联剪切检查:若 未被打标则打标;若 已被打标则同样将 剪切至根链表并继续向上回溯。
- 将 从其父节点 的子链表中剪切下来,加入到根链表中,并将 重置为
这一规则限制了任何非根节点子树的“萎缩”程度。
树规模下界与最大度数证明
子节点度数引理
设 为任意节点,将其当前的所有子节点按照挂载到 的先后时间顺序排列为 。
引理:对于第 个子节点 (),其当前度数满足:
证明:当 被链接为 的子节点时, 此前至少已经挂载了 共 个子节点,因此当时 的度数至少为 。因为只有度数相同的根树才会合并,所以 在挂载瞬间的度数也至少为 。在挂载之后, 至多因为级联剪切失去 1 个子节点(若失去 2 个则自身早已被剪切)。因此 目前保留的子节点度数至少为 。对于 ,显然 。
斐波那契数列下界与最大度数
设 为度数为 的任意节点的最小可能子树规模。显然 。对于 :
利用数学归纳法易证:
其中 为标准斐波那契数()。由斐波那契数列的通项公式可知:
因此,在包含 个节点的 Fibonacci 堆中,任何节点的度数 必须满足:
这证明了堆中任意节点的最大度数 。
势能法均摊复杂度推导
势能函数设计
定义 Fibonacci 堆 的势能函数为:
其中:
- :根链表中树的棵数(根节点总数);
- :整座森林中被标记(
mark == true)的节点总数。
由于 ,初始空堆 且任意时刻 。因此均摊代价的上界是真实总操作时间的有效上界。
操作均摊代价计算
操作的均摊代价定义为:。
1. Insert(x)
- 实际代价 ;
- 增加了一个根节点:;标记节点未变:;
- 势能变化 ;
- 均摊代价:。
2. Union(H1, H2)
- 实际代价 ;
- 根链表拼接,总根数和标记数均不变:;
- 均摊代价:。
3. Extract-Min(H)
- 设提取前根树数量为 。实际代价包含遍历 个子节点和扫描合并 个根节点,实际耗时 ;
- 合并整理后,每个度数至多保留 1 棵树,因此最终根数至多为 ;
- 根数变化:;标记数未增加:;
- 势能变化:;
- 均摊代价:
项完全被势能下降抵消!
4. Decrease-Key(x, k)
- 假设减小键值触发了 次级联剪切(剪切了 以及向上连续剪切了 个已标记祖先节点);
- 实际代价为遍历链表的 ;
- 变化统计:
- 产生了 棵新根树加入根链表:;
- 剪切下来的 个节点标记被重置为
false(减少了至少 个原标记节点); - 最顶层停止处的父节点可能由未标记变为标记(增加至多 1 个标记);
- 净标记变化:;
- 势能变化:
- 均摊代价:
级联剪切的级联耗时完全由此前标记时积累的势能买单,均摊恒为 !
理论突破与工程取舍
Fibonacci 堆是理论算法设计中的一座丰碑,成功将 Dijkstra 和 Prim 算法推至 。
然而在工业工程实践中(例如标准库实现),Fibonacci 堆极少被直接采用:
- 常数因子巨大:每个节点维护 4 个指针、1 个键、1 个度数和 1 个布尔标记,内存开销大且存在大量的指针间接寻址与缓存不友好(Cache-Miss)。
- 非均摊的最坏情况波动:单次
Extract-Min或Decrease-Key可能消耗 最坏时间,对实时性敏感系统不友好。 - 替代结构的崛起:配对堆(Pairing Heap)在实践中速度显著超越 Fibonacci 堆;而在特定整数场景下,基数堆与多级桶更为高效。
我的理解
我的理解
势能法的本质是构建“时间银行”: Fibonacci 堆的设计哲学是均摊分析的典范。算法在平时(插入、合并、打标)极其懒惰,只做最微小的指针重连,但通过势能函数中的 向“时间银行”预存了代币。当发生昂贵操作(级联剪切或大批量根合并)时,每一笔耗时的遍历都必然伴随着标记的消除或根的配对,从而直接支取预存的代币冲抵开销。
标记机制的精妙之处在于“一次宽恕”:允许节点失去一个孩子(适应动态调整),但在失去第二个孩子时立即进行激进修复(保证度数的对数界)。这一局部松弛与全局约束的张力平衡,正是理论计算机科学最深邃的智慧之一。