贪心算法(哈夫曼编码以及最小生成树:Kruskal算法和Prim算法)

贪心问题

具有的性质如下:
贪心选择性:每一步贪心选出来的一定是原问题的最优解的一部分
最优子结构:每一步贪心选完后会留下子问题,子问题的最优解和贪心选出来的解可以凑成原问题的最优解

哈夫曼编码

贪心选择性:每次贪心选频率最低的两个,那频率最低的两个的编码一定是原问题的最优解的一部分;
最优子结构:每次贪心选走的两个后,剩下的最优编码和贪心选走的那两个的最优编码一起可以合成原问题的最优编码。

前缀码

定义

没有任何码字是其他码字的前缀

作用
简化解码过程:没有任何一个字符的编码是另一个字符编码的前缀。这样的编码具有唯一可解析性,即通过编码可以唯一地解码出原始的字符序列。这是因为在解码时,我们可以一直读取编码,直到找到与某个字符匹配的编码为止,而不会有歧义。

解码过程需要前缀码的一种方便的表示形式:
二叉树

  • 其叶节点为给定的字符

  • 字符的二进制码字用从根节点到该字符叶节点的简单路径表示

最优编码方案:

总是对应一颗满二叉树

给定一棵对应前缀码的树T,对于字母表𝐶中的每个字符𝑐,令属性𝑐. 𝑓𝑟𝑒𝑞表示𝑐在文件中出现的频率,令𝑑𝑇( 𝑐) 表示𝑐的叶结点在树中的深度。𝑑𝑇( 𝑐)也是字符𝑐的码字的长度。

编码文件需要的二进制位个数(T的代价):

B ( T ) = ∑ c ∈ C c . f r e q ∗ d T ( c ) B(T) = \sum _{c \in C} c.freq * d_T(c) B(T)=cCc.freqdT(c)

最优前缀码对应的编码树具有最小的代价

**赫夫曼编码 (Huffman code)。**假定C是𝑛个字符的集合,其中每个字符𝑐 ∈ 𝐶都是一个 对象,其属性𝑐. 𝑓𝑟𝑒𝑞给出了字符的出现频率。算法自底向上地构造出对应最优编码的二叉树𝑇。它从∣ 𝐶 ∣个叶结点开始,执行∣ 𝐶 ∣ −1个“合并” 操作创建出最终的二叉树。算法使用一个以属性𝑓𝑟𝑒𝑞为关键字的最小优先队列𝑄,以识别两个最低频率的对象将其合并。当合并两个对象时, 得到的新对象的频率设置为原来两个对象的频率之和

在这里插入图片描述
在这里插入图片描述

贪心选择性质

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择得到,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。(在动态规划方法中,每个步骤都要进行一次选择,但选择依赖于子问题的解,也因此我们通常以一种自底向上的方式求解动态规划问题。当然我们也可以自顶向下求解,但需要备忘机制,即使是自顶向下进行计算,我们仍需要先求解子问题再进行选择)

证明思路:

先考察一个全局最优解,然后对该解加以修改(一般是采用“剪枝”技巧),使其采用贪心选择,这个选择将原问题变成一个相似的、但是更小的问题

验证正确性:

证明确定最优前缀码的问题具有贪心选择最优子结构性质

  • 下面的引理证明问题具有贪心选择性质

引理: 令C为一个字母表,其中每个字符c ∈ C都有一个频率c. freq。令x和y是C中频率最低的两个字符。那么存在C的一个最优前缀码,x和y的码字长度相同,且只有最后一个二进制位不同。

整个证明思路分为两步:

1)证最优前缀码树是一棵叶结点满二叉树(证性质2必须要用这个性质)

2)令T是最优前缀码树,证明x,y码长相同且仅最后一位不同(须说明存在一个最优前缀码树的最深的那条路径的两个叶子节点就是x,y,这需要小小的构造一下,利用它们频度最低的这个性质)。

img

img

img

img

最优子结构性质

在这里插入图片描述
在这里插入图片描述

最小生成树

什么是最小生成树?

最小生成树(Minimum Spanning Tree,简称 MST)是在一个连通且带权(权值非负)无向图中找到一棵包含所有节点,使得树上边的权值之和最小

解决最小生成树问题的两种算法:

Kruskal算法和Prim算 法。

都是贪心算法。

采用的贪心策略可以由下面的通用方法来表述。该通用方法在每个时刻生长最小生成树的一条边,并在整个策略的实施过程中,管理一个遵守下述循环不变式边集合A

在每次循环之前,A是某棵最小生成树的一个子集。

在每一步,我们要做的事情是选择一条边( 𝑢, 𝑣) ,将其加入到A中,使A不违反循环不变式,即𝐴 ∪ { (𝑢, 𝑣) }也是某棵最小生成树的子集。由于我们可以安全地将这种边加入到集合A而不会破坏A的循环不变式,因此称这样的边为集合A的安全边

如何找到安全边?

首先了解一些定义:切割(将图中的顶点集合分成两个互不相交的部分),横跨边(两个部分中间连的边),尊重(没横跨边),轻量级边(横跨边中权重最小的边)。

在这里插入图片描述

定理 设𝐺 = (𝑉, 𝐸) 是一个在边𝐸上定义了实数值权重函数𝑤的连通无向图。设集合𝐴为𝐸的一个子集,且𝐴包括在图𝐺的某棵最小生成树中,设 (𝑆, 𝑉 − 𝑆) 是图𝐺中尊重集合𝐴的任意一个切割,又设 (𝑢, 𝑣) 是横跨切割( 𝑆, 𝑉 − 𝑆 )的一条轻量级边。那么边 (𝑢, 𝑣) 对于集合𝐴是安全的。

证明: 设𝑇是一棵包括𝐴的最小生成树,并假定𝑇不包含轻量级边( 𝑢, 𝑣 );否则,证明完毕。现在来构建另一棵最小生成树𝑇 ′ ,通过剪切和粘贴将𝐴 ∪ { 𝑢, 𝑣 }包括在 树𝑇 ′中,从而证明 (𝑢, 𝑣) 对于集合𝐴来说是安全的。 边 (𝑢, 𝑣) 与𝑇中从结点𝑢到结点𝑣的简单路径𝑝形成一个环路,如下图所示。由于𝑢 和𝑣分别处在切割 (𝑆, 𝑉 − 𝑆) 的两端,𝑇中至少有一条边属于简单路径𝑝并且横跨该切割。设 (𝑥, 𝑦) 为这样的一条边。因为切割 (𝑆, 𝑉 − 𝑆) 尊重集合𝐴,边 (𝑥, 𝑦) 不在集合𝐴中。由于边 (𝑥, 𝑦) 位于𝑇中从𝑢到𝑣的唯一简单路径上,将该条边删除会导致𝑇 被分解为两个连通分量。将 𝑢, 𝑣 加上去可将这两个连通分量连接起来形成一棵新的生成树𝑇 ′ = 𝑇 − { 𝑥, 𝑦 } ∪ { 𝑢, 𝑣 }。

在这里插入图片描述

在这里插入图片描述

下面证明𝑇 ′是一棵最小生成树。由于边 (𝑢, 𝑣) 是横跨切割 (𝑆, 𝑉 − 𝑆) 的一条轻量级边并且边 (𝑥, 𝑦) 也横跨该切割,我们有𝑤 (𝑢, 𝑣) ≤ 𝑤 (𝑥, 𝑦) 。因此,𝑤 (𝑇 ′) = 𝑤 (𝑇) − 𝑤 (𝑥, 𝑦) + 𝑤 (𝑢, 𝑣) ≤ 𝑤 (𝑇).但是,𝑇是一棵最小生成树,有𝑤 (𝑇) ≤ 𝑤 (𝑇 ′) ;因此,𝑇 ′也是一棵最小生成树。

下面还需要证明**边 (𝑢, 𝑣) 对于集合𝐴来说是一条安全边。**因为𝐴 ⊆ 𝑇并且 (𝑥, 𝑦) ∉ 𝐴, 所以有𝐴 ⊆ 𝑇 ′;因此𝐴 ∪ { 𝑢, 𝑣 } ⊆ 𝑇 ′。由于𝑇 ′是最小生成树, (𝑢, 𝑣) 对于集合𝐴是 安全的。

Kruskal算法和Prim算法将使用下列推论。

推论: 设𝐺 = (𝑉, 𝐸) 是一个连通无向图,并有定义在边集合𝐸上的实数值权重函数 𝑤。设集合𝐴为𝐸的一个子集,且该子集包括在G的某棵最小生成树里,并设𝐶 = (𝑉𝐶, 𝐸𝐶) 为森林𝐺𝐴 =( 𝑉, 𝐴) 中的一个连通分量(树)。如果边 (𝑢, 𝑣) 是连接𝐶和𝐺𝐴 中其他连通分量的一条轻量级边,则边 (𝑢, 𝑣) 对于集合𝐴是安全的。

Kruskal算法:

Kruskal算法找到安全边的办法是,在所有连接森林中两棵不同树的边里面,找到权重最小的边 (𝑢, 𝑣) 。设𝐶1和𝐶2为边 (𝑢, 𝑣) 所连接的两棵树。由 于边 (𝑢, 𝑣) 一定是连接𝐶1和(某棵)其他树的一条轻量级边,前述推论告诉我们,边 (𝑢, 𝑣) 是𝐶1的一条安全边。Kruskal算法属于贪心算法,因为它每次都选择一条权重最小的边加入到森林。

在这里插入图片描述

使用一个不相交集合数据结构来维护几个互不相交的元素集合。每个集合代表当前森林中的一棵树。操作FIND-SET(u)用来返回包含元素𝑢的集合的代表元素。我们可以通过测试FIND-SET(u)是否等于 FIND-SET(v)来判断结点𝑢和结点𝑣是否属于同一棵树。Kruskal算法使用 UNION过程来对两棵树进行合并。

Kruskal算法的运行时间依赖于不相交集合数据结构的实现方式。假定使用不相交集合森林(目前已知的不相交集合数据结构的渐近时间最快的实现方式)实现。 在这种实现模式下,算法第1行对集合𝐴的初始化时间为𝑂 (1) ,第4行对边进行排 序的时间为𝑂 (𝐸 𝑙𝑔 𝐸) ,第5~8行的for循环执行𝑂 (𝐸) 个FIND-SET和UNION操作。与 ∣ 𝑉 ∣个MAKE-SET操作一起,这些操作的总运行时间为𝑂(( 𝑉 + 𝐸)* α( 𝑉)) ,这里α是 一个增长非常缓慢的函数。假定图𝐺是连通的,有∣ 𝐸 ∣≥∣ 𝑉 ∣ −1,所以不相交集合操作的时间为𝑂( 𝐸*α (𝑉)) 。由于α(𝑉) = 𝑂 (𝑙𝑔 𝑉) = 𝑂 (𝑙𝑔 𝐸) ,Kruskal算法的总运行时间为𝑂 (𝐸 𝑙𝑔 𝐸) 。再注意到∣ 𝐸 ∣<∣ 𝑉 ∣2 ,有𝑙𝑔 𝐸 = 𝑂 (𝑙𝑔 𝑉) ,因此,可将 Kruskal算法的时间表示为𝑶 (𝑬 𝒍𝒈𝑽) 。

Prim算法:

Prim算法的工作原理与Dijkstra最短路径算法相似)。Prim算法具有的一个性质是集合A中的边总是构成一棵树。这棵树从一个任意的根结点𝑟开始, 一直长大到覆盖V中的所有结点时为止。算法每一步在连接集合A和A之外的结点 的所有边中,选择一条轻量级边加入到A中。根据前述的推论,这条规则所加入 的边都是对A安全的边。因此,当算法终止时,A中的边形成一棵最小生成树。本 策略也属于贪心策略,因为每一步所加入的边都是使树的总权重增加量最小的边。

在这里插入图片描述

Prim算法的运行时间取决于最小优先队列𝑄的实现方式。

如果将𝑄实现为一个二叉最小优先队列,while循环中的语句一共要执行∣ 𝑉 ∣次,由于每个EXTRACT-MIN 操作需要的时间成本为𝑂 (𝑙𝑔 𝑉) ,EXTRACT-MIN操作的总时间为𝑂 (𝑉 𝑙𝑔 𝑉) 。由于所有邻接链表的长度之和为2 ∣ 𝐸 ∣,算法第8~11行的for循环的总执行次数为𝑂 (𝐸) 。 在for循环里面,我们可以在常数时间内完成对一个结点是否属于队列𝑄的判断, 方法就是对每个结点维护一个标志位来指明该结点是否属于𝑄。算法第11行的赋 值操作涉及一个DECREASE-KEY操作,该操作在二叉最小堆上执行的时间成本为 𝑂 (𝑙𝑔 𝑉) 。

因此,Prim算法的总时间代价为𝑂 (𝑉 𝑙𝑔 𝑉 + 𝐸 𝑙𝑔 𝐸) = 𝑶 (𝑬 𝒍𝒈𝑽) 。从渐近意义上来说,它与Kruskal算法的运行时间相同。

如果使用斐波那契堆来实现最小优先队列𝑄,则EXTRACT-MIN操作的时间摊还代 价为𝑂 (𝑙𝑔 𝑉) ,而DECREASE-KEY操作的摊还时间代价为𝑂 1 。因此,如果使用斐 波那契堆来实现最小优先队列𝑄,则Prim算法的运行时间将改进到𝑶 (𝑬 + 𝑽 𝒍𝒈𝑽) 。

  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值