[算法分析与设计] 2. 斐波那契堆及其应用

这篇具有很好参考价值的文章主要介绍了[算法分析与设计] 2. 斐波那契堆及其应用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一个优先队列需要支持的操作有

  • insert 插入元素 \(x\)
  • find-min 返回最小的元素。
  • delete-min 删除最小的元素。
  • decrease-key 将一个元素 \(x\) 减小 \(k\)\(k \geq 0\)

常用于实现优先队列的数据结构是

需要注意的是,小根堆需要支持 decrease-key,大根堆需要支持 increase-key。对于一个堆来说两种操作的难度并不一致。以下均考虑小根堆。

还有一个可能支持的操作

  • meld 合并两个堆。也叫做 union / merge。

定义 一个堆是一种基于树的数据结构,满足堆性质

  • \(A\)\(B\) 的父亲,则 \(A\) 的键值比 \(B\) 的键值小。

一些常见的优先队列实现方法的复杂度表

操作 链表 二叉堆 二项堆 斐波那契堆† Brodal queue
insert \(O(1)\) \(O(\log n)\) \(O(\log n)\) \(O(1)\) \(O(1)\)
delete-min \(O(n)\) \(O(\log n)\) \(O(\log n)\) \(O(\log n)\) \(O(\log n)\)
decrease-key \(O(n)\) \(O(\log n)\) \(O(\log n)\) \(O(1)\) \(O(1)\)
meld \(O(1)\) \(O(n)\) \(O(\log n)\) \(O(1)\) \(O(1)\)
find-min \(O(n)\) \(O(1)\) \(O(1)\) \(O(1)\) \(O(1)\)

delete 能归约到 decrease-key + delete-min,并且一般不会比这俩优,所以不单独写了。

† 表示均摊复杂度。

我们先不介绍这些堆,先来看我们有了这些黑盒后如何优化一些算法。一般的堆对算法的优化是广为人知的,Brodal queue 的实现又过于复杂并且只是斐波那契堆的复杂度变严格,所以接下来的例子均为斐波那契堆的应用。

一个著名的例子是对 Dijkstra 算法的优化。如果我们用 decrease-key 处理最短路数组的更新,Dijkstra 会进行 \(n\) 次 delete-min,\(n\) 次 insert,\(m\) 次 decrease-key。使用斐波那契堆可以做到 \(O(m + n \log n)\)

类似地,Prim 算法会进行 \(n\) 次 delete-min,\(n\) 次 insert,\(m\) 次 decrease-key,因此可以做到 \(O(m + n \log n)\)。但和 Dijkstra 不同的是,这不是最优的算法。最小生成树问题弱于排序问题。

优化点在于最小生成树的割性质:对于任意的 \(S \subseteq V\)\(S\)\(V \setminus S\) 之间的边中边权最小的那条一定在任一最小生成树之中。这代表着我们把点集划分为若干个集合,每个集合作为一个新点,跨越两个集合的边连在新点上得到的新图的最小生成树也会在旧图上成立。由此我们可以递归。Borůvka 算法就是这么做的。

我们从一个点开始做 Prim,到堆的大小 \(= k\) 时停止,把涉及到的点与其连出的边合并为一个新点,再在新图中找一个没有被访问过的点继续做 Prim(也需要考虑和新点之间的连边,否则正确性无法保证),这样最后每个新点的度数都 \(\geq k\),因此至多有 \(\frac {2m}k\) 个集合。根据割性质,现在旧图上最小生成树剩下的边可以通过求新图上的最小生成树得到,因此可以递归这个过程。

考虑令 \(k = 2^{\frac {2m}n}\) 则每轮的复杂度 \(O(m + n \log k) = O(m)\),并且

\[\frac{2m}{n'} \geq k = 2^{\frac{2m}{n}} \]

因此第 \(i\) 轮满足 \(\log^{(i)} \frac{2m}{n'} \geq \frac {2m}n\),需要的轮数与 \(\min_i\left\{\log^{(i)} n \leq \frac mn\right\} =: \beta(m, n)\) 同阶,因此总复杂度为 \(O(m \beta(m, n))\)。当 \(m = O(n)\) 时,复杂度也可写为 \(O(m \log^* n)\)

这个算法仍然不是最优的。

  • [Karger, Klein, Tarjan '95] \(O(m)\) 的随机算法。
  • [Chazelle '00] \(O(m\alpha(m, n))\) 的确定性算法。
  • [Pettie, Ramachandran '02] 证明了的最优算法,但是复杂度确界没有给出,仅知道其在 \(\Omega(m)\)\(O(m \alpha(m, n))\) 中。
  • [Dixon, Rauch, Tarjan '92] \(O(m)\) 验证一棵树是否是最小生成树。

接下来我们来介绍二项堆和斐波那契堆。

二项树 \(B_n\) 是递归定义的一种树。\(B_0\) 是一个单点,\(B_n\) 把两棵 \(B_{n-1}\) 的根节点相连,并把其中一棵的根节点认作新的根节点。\(|B_n| = 2^n\)。定义 \(B_n\)\(\mathrm{rank}\)\(n\)

一个二项堆包含若干棵二项树,满足每棵树都满足堆性质,且它们的的 \(\mathrm{rank}\) 互不相同。

由于 \(|B_n| = 2^n\),把二项堆的大小 \(m\) 写成二进制,值为 \(1\) 的那些数位恰好对应一棵二项树。因此二项堆中树的棵数为 \(O(\log m)\)

meld 如果有两个相同 \(\mathrm{rank}\) 的二项树,那么可以将两者合并,将键值小的作为新的根节点,得到一个 \(\mathrm{rank}+1\) 的仍然满足堆性质的二项树。我们可以将两个堆的大小 \(m_1, m_2\) 写成二进制,列竖式做加法,相同数位的树便也做加法,进位到下一位上。

每一次合并的复杂度是 \(O(1)\),因此总复杂度为 \(O(\log m)\)

insert 将原树和一个仅包含 \(B_0\) 的树做 meld。复杂度为 \(O(\log m)\)

delete-min 在所有二项树中找到根节点键值最小的,将其所有儿子和剩下的二项树做 meld。复杂度为 \(O(\log m)\)

find-min 每次 delete-min 的时候维护一下。复杂度为 \(O(1)\)

decrease-key 向上冒泡。因为树深等于其 \(\mathrm{rank}\),复杂度为 \(O(\log m)\)

斐波那契堆基于二项堆的思路。它的大致想法是,与其每次都严格维护二项树树形,不如把需要的点直接从树上拆下来,直到 delete-min 的时候再重构,用势能分析得到均摊复杂度。

一个斐波那契堆 \(H\) 仍然包含若干棵树,每一棵树都是删去了一些子树的二项树。那些被删去了儿子的点称为标记点。

定义 \(\mathrm{rank}(x)\)\(x\) 的儿子个数。\(\mathrm{rank}(H) = \max_{x \in H} \mathrm{rank}(x)\)\(\mathrm{tree}(H)\)\(H\) 拥有的树的棵数,\(\mathrm{marks}(H)\)\(H\) 的标记点的个数。

insert 直接将一个 \(B_0\) 放入 \(H\)\(\mathrm{tree}(H)\) 加一。

delete-min 将其所有儿子放入 \(H\) 中,并将 \(\mathrm{rank}\) 相同的两棵树同二项树一样合并,直到没有 \(\mathrm{rank}\) 相同的两棵树。

decrease-key 如果操作后堆性质仍然满足,不做任何事。否则其键值小于父亲的键值,此时直接将这棵子树从树上拆下来,并将其父亲设为标记点。如果其父亲已经为标记点,则将这个单点也拆下来,删去其标记,并再次考虑它的父亲,循环这个过程直到一个未被标记的点或者根,我们总是不标记根。

这么做的动机是,一棵深的树必须也得是重的。如果删去了太多儿子,可能会导致其结点数很少但是深度很深,从而拖累复杂度。所以我们虽然允许拆子树,但只允许一个结点拆一次,否则这棵子树就要送去重构。在之后的分析中我们会看到满足条件的最小的树的节点个数呈斐波那契数列(直观地,\(|B_n| = |B_{n-1}| + |B_{n-1}| \Rightarrow |B'_n| \geq |B'_{n-1}| + |B'_{n-1\color{red}{-1}}|\)),这也是其名字的由来。

meld 把所有树放到一起就好了。

接下来进行复杂度分析和一些实现细节的描述。

先来考虑形式化上面说的那段话。令 \(F_n\) 表示 \(\mathrm{rank} = n\) 的最小的树的大小,满足性质

  • \(y_1, \ldots, y_k\) 是结点 \(x\) 的儿子按照 \(\mathrm{rank}\) 从小到大排序后的序列,则 \(\mathrm{rank}(y_i) \geq \max(0, i - 2)\)

这条性质是一个序列可以对应斐波那契堆中一棵树在某个时候的形态的充要条件。因为我们总是合并 \(\mathrm{rank}\) 相同的树,当 \(x\) 已有 \(y_1, \ldots, y_{i-1}\) 时,称其为 \(x_i\),合并了 \(y_i\) 则有 \(\mathrm{rank}(y_i) = \mathrm{rank}(x_i) \geq i - 1\),由于 \(y_i\) 至多失去一个孩子,\(\mathrm{rank}(y_i) \geq i - 2\)。而按照这个过程我们也能构建出一棵树与对应的拆子树操作来。

则一个 \(B'_n\) 可以通过某个 \(B'_{n-1}\) 加入第 \(n\) 个儿子得到,即加入某个 \(B'_{n-2}\) 得到,因此有 \(F_n = F_{n-1} + F_{n-2}\)\(F_0 = 1, F_1 = 2\),因此其是斐波那契数列,满足 \(F_n \geq \left(\frac{1 + \sqrt 5}2\right)^n\),故树深总是树重的 \(\log\)

接下来考虑势能函数。有两个操作需要均摊分析,一个是树的个数,一个是标记的个数,前者在 delete-min 中用到,后者在 decrease-key 向上递归时用到。

\(\Phi(H) = \mathrm{tree}(H) + 2 \mathrm{marks}(H)\),之所以要加倍数 \(2\) 是因为将标记点拆下来时 \(-\Delta\mathrm{marks}(H) = \Delta\mathrm{tree}(H)\),但我们要统计这部分的实际开销,如果不加倍数则势能不变。因此势能里两者要相差一倍,加给谁都可以,但是既然 \(\mathrm{tree}(H)\) 要承担更多的操作不如让它是一倍的那位。

现在开始考虑每个操作的实际开销与势能变化。

insert 实际开销 \(O(1)\),势能变化 \(+1\),复杂度 \(O(1)\)

delete-min 实际开销 \(O(\mathrm{rank}(H)) + O(\mathrm{tree}(H))\),势能变化 \(\mathrm{tree}(H') - \mathrm{tree}(H) = O(\mathrm{rank}(H')) - \mathrm{tree}(H)\),均摊复杂度 \(O(\mathrm{rank}(H)) + O(\mathrm{rank}(H')) = O(\log n)\)

decrease-key 实际开销 \(O(c)\)\(c\) 为涉及的标记点个数,势能变化 \(\leq c + 2(-c + 2) = O(1) - c\),均摊复杂度 \(O(1)\)。需要对儿子维护一个 \(O(1)\) 删除、线性时间遍历的数据结构,一个简单的实现是双向循环链表。

meld 如果使用了双向循环链表那 \(O(1)\) 的合并是简单的。文章来源地址https://www.toymoban.com/news/detail-711661.html

到了这里,关于[算法分析与设计] 2. 斐波那契堆及其应用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【算法】斐波那契数列与台风的故事

    在小岛的一个海滨小镇上,住着一个名叫苏菲的女孩。苏菲一家人靠海为生,她的生活简单而朴素,与大自然和谐共生。每天,苏菲都会来到海边,欣赏那美丽的日出和日落,感受着大海的呼吸。 然而,小岛的美丽风光并非一成不变。每年夏季,热带气旋活跃,台风频繁登陆

    2024年02月10日
    浏览(43)
  • 【算法】斐波那契数列通项公式

    如果数列 a n a_n a n ​ 的递推公式: a n = c 1 a n − 1 + c 2 a n − 2 a_n=c_1a_{n-1}+c_2a_{n-2} a n ​ = c 1 ​ a n − 1 ​ + c 2 ​ a n − 2 ​ ------(1) 根据待定系数法,假设 a n − x a n − 1 = y ( a n − 1 − x a n − 2 ) a_n-xa_{n-1}=y(a_{n-1}-xa_{n-2}) a n ​ − x a n − 1 ​ = y ( a n − 1 ​ − x a n − 2 ​

    2023年04月24日
    浏览(49)
  • 【算法学习】斐波那契数列模型-动态规划

            我在算法学习过程中,针对斐波那契数列模型的动态规划的例题进行了一个整理,并且根据标准且可靠一点的动态规划解题思路进行求解类似的动归问题,来达到学习和今后复习的必要。         所谓的斐波那契数列模型,即当前状态的值等于前两种状态的值之和。

    2024年02月04日
    浏览(56)
  • Java与查找算法(3):斐波那契查找

    Java面试资料 Java面试资料,涵盖:面试前准备,面试经验,面试真题等 链接:Java面试资料,面试准备、面试题,面试真题 斐波那契查找是一种基于二分查找的查找算法,它使用了斐波那契数列的特性来确定查找点的位置,从而提高了查找效率。斐波那契查找的时间复杂度为

    2024年02月06日
    浏览(39)
  • 算法:动态规划---斐波那契和最短路径

    从本篇开始总结的是动态规划的一些内容,动态规划是算法中非常重要的一个版块,因此也是学习算法中的一个重点,在学习动态规划前应当要把动态规划的基础知识学习一下 动态规划既然是一个新的算法,这个名字也是新名字,那么就要首先明确这个算法的名字代表什么含

    2024年01月25日
    浏览(56)
  • 【算法系列 | 12】深入解析查找算法之—斐波那契查找

    心若有阳光,你便会看见这个世界有那么多美好值得期待和向往。 决定开一个算法专栏,希望能帮助大家很好的了解算法。主要深入解析每个算法,从概念到示例。 我们一起努力,成为更好的自己! 今天第12讲,讲一下查找算法的—斐波那契查找 斐波那契查找算法是一种

    2024年02月03日
    浏览(54)
  • C语言经典算法实例6:斐波那契数列

    斐波那契数列指的是这样一个数列:1,1,2,3,5,8,13,21,34,55,89… 这个数列从第3项开始,每一项都等于前两项之和。 斐波那契数列的定义者,是意大利数学家莱昂纳多·斐波那契(Leonardo Fibonacci),生于公元1170年,卒于1250年,籍贯是比萨。 他被人称作“比萨的莱昂

    2024年02月02日
    浏览(46)
  • 解锁动态规划:从斐波那契到高效算法

    👤作者介绍:10年大厂数据经营分析经验,现任大厂数据部门负责人。 会一些的技术:数据分析、算法、SQL、大数据相关、python 欢迎加入社区:码上找工作 作者专栏每日更新: LeetCode解锁1000题: 打怪升级之旅 python数据分析可视化:企业实战案例 备注说明:方便大家阅读,

    2024年04月15日
    浏览(39)
  • 【算法优选】 动态规划之斐波那契数列模型

    动态规划相关题目都可以参考以下五个步骤进行解答: 状态表⽰ 状态转移⽅程 初始化 填表顺序 返回值 后面题的解答思路也将按照这五个步骤进行讲解。 泰波那契序列 Tn 定义如下: T0 = 0, T1 = 1, T2 = 1, 且在 n = 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2 给你整数 n,请返回第 n 个泰波那契

    2024年02月05日
    浏览(56)
  • C++算法 —— 动态规划(1)斐波那契数列模型

    每一种算法都最好看完第一篇再去找要看的博客,因为这样会帮你梳理好思路,看接下来的博客也就更轻松了。当然,我也会尽量在写每一篇时都可以让不懂这个算法的人也能边看边理解。 动规的思路有五个步骤,且最好画图来理解细节,不要怕麻烦。当你开始画图,仔细阅

    2024年02月10日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包