算法设计与分析 (4. 贪心算法, 未完)

贪心算法的基本思想

用¥100买书一本, 花去¥29.7, 如果要找最少张数的钱, 需要如何找, 多少张?
某国家的钱分20, 11, 5, 1 cents四种, 如果用20cents买5cents的东西, 需要如何找使得钱的张数最少?

分析

  • 贪心算法并不从整体最优考虑, 它所做出的选择只是在某种意义下的局部最优选择.
  • 贪心算法的优点:时间复杂度低.

4.1 活动安排问题

问题提出

各活动起止时间如向量 s s s f f f, 最多可安排多少活动?给出一个解.
s = [ 1 , 2 , 4 , 3 , 2 , 6 , 7 , 6 ] s = [1, 2, 4, 3, 2, 6, 7, 6] s=[1,2,4,3,2,6,7,6]
f = [ 5 , 3 , 5 , 6 , 7 , 8 , 8 , 7 ] f = [5, 3, 5, 6, 7, 8, 8, 7] f=[5,3,5,6,7,8,8,7]

解决方案

各活动的开始时间和结束时间存储于数组 s s s f f f 中, 且按结束时间的非减序排列

i i i 1 2 3 4 5 6 7 8 9 10 11
s [ i ] s[i] s[i] 1 3 0 5 3 5 6 8 8 2 12
f [ i ] f[i] f[i] 4 5 6 7 8 9 10 11 12 13 14

选择第一个活动, 依次跳过后面与其冲突的活动, 再选择第一个与之不冲突的活动, 以此类推.

/**
 * Title:        活动安排问题.
* Version: 1.0
* Copyright: Copyright (c) 2003, all rights reserved
* Author: 闵帆
* Company: www.fansmale.com
* Written time: 2003/09/02
* Last modify time: 2021/11/30
*/ public class GreedySelector{ /** 入口方法. */ public static void main(String args[]){ int [] startTimes = {-1, 1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12}; int [] finishTimes = {-1, 4, 5, 6, 7, 8, 9,10,11, 12,13,14}; boolean [] isSelected = new boolean[12]; int successActivity = greedySelector(startTimes, finishTimes, isSelected); for(int i = 1; i <= 11; i++){ if (isSelected[i]){ System.out.print(startTimes[i] + "->" + finishTimes[i] + "\t"); }//Of if }//Of for System.out.println("\n" + successActivity + " activities are successfully scheduled."); }//Of main /** ********************************* * 获取最优解, 即最多活动数. * @param s 开始时间向量. * @param f 结束时间向量. * @param a 是否安排. ********************************* */ public static int greedySelector(int []s, int []f, boolean a[]){ int n = s.length - 1; a[1] = true; int j = 1; int count = 1; for (int i = 2; i <= n; i ++){ if (s[i] >= f[j]){ a[i] = true; j = i; count ++; }else{ a[i] = false; }//Of if }//Of for return count; }//Of greedySelector }//Of class GreedySelector

结果显示

1->4	5->7	8->11	12->14	
4 activities are successfully scheduled.

算法复杂度分析

  • 排序花费时间
  • 选择花费时间

算法正确性证明

条件:活动按结束时间非递减排序
证明:
(a) 令 P P P 为活动全集, A = { a 1 , a 2 , … , a k } ⊆ P A = \{a_1, a_2, \dots, a_k\} \subseteq P A={ a1,a2,,ak}P 为一个最优活动集合, 其中 f [ a 1 ] ≤ f [ a 2 ] ≤ ⋯ ≤ f [ a k ] f[a_1] \leq f[a_2] \leq \dots \leq f[a_k] f[a1]f[a2]f[ak]. 由活动的相容性可知, f [ a 1 ] ≤ s [ a i ] f[a_1] \leq s[a_i] f[a1]s[ai], 其中 2 ≤ i ≤ k 2 \leq i \leq k 2ik. 将活动 a 1 a_1 a1 替换为活动 1 1 1 可得 A ′ = { 1 , a 2 , … , a k } A' = \{1, a_2, \dots, a_k\} A={ 1,a2,,ak}. 由条件可知 f [ 1 ] ≤ f [ a 1 ] ≤ s [ a i ] f[1] \leq f[a_1] \leq s[a_i] f[1]f[a1]s[ai], 其中 2 ≤ i ≤ k 2 \leq i \leq k 2ik. 即 A ′ A' A 为一个相容活动集合. 由于 ∣ A ′ ∣ = ∣ A ∣ \vert A' \vert = \vert A \vert A=A, A ′ A' A 也为一个最优活动集合. 因此, 活动 1 1 1 至少包含在某个最优解之内.
(b) 将活动 1 1 1 及与其冲突的活动从 P P P 中删除, 获得活动子集 P ′ P' P, 并讨论其活动安排问题. 由于 a i a_i ai 不与活动 1 1 1 冲突, a i ∈ P ′ a_i \in P' aiP, 其中 2 ≤ i ≤ k 2 \leq i \leq k 2ik. 又由于 a 2 , … , a k a_2, \dots, a_k a2,,ak 相互不冲突, 所以 { a 2 , … , a k } \{a_2, \dots, a_k\} { a2,,ak} P ′ P' P 中可安排的活动集. 即 P ′ P' P 中至少可以安排 k − 1 k - 1 k1 个活动. 另一方面 P ′ P' P 中不可能安排多于 k − 1 k - 1 k1 个活动, 否则加上活动 1 1 1, P P P 中可安排的活动数将超过 k k k, 这与 A ′ A' A 是最优活动集合相矛盾. 因此, { a 2 , … , a k } \{a_2, \dots, a_k\} { a2,,ak} P ′ P' P 的最优活动集合.
综上所述, (a) 证明了贪心选择性质, (b) 证明了最优子结构. 因此算法的正确性得证.

习题4-2

在活动安排问题中, 还可以有其他的贪心选择方案, 但并不能保证产生最优解. 给出一个例子, 说明若选择具有最短时段的相容活动作为贪心选择, 得不到最优解.

4.2 贪心算法的基本要素

  • 4.2.1 贪心选择性质
    可以自顶向下, 而不需要自底向上的方式. 每步所做选择不依赖于将来所做的选择, 也不依赖于子问题的解.
  • 4.2.2 最优子结构性质
    问题的最优解包含其子问题的最优解 (与动态规划相同)

自顶向下与自底向上

  • 自顶向下
    直接把问题变成子问题. 分析问题与解决问题方向一致. 如找零钱问题、活动安排问题等.
  • 自底向上
    把子问题堆为原问题. 分析问题与解决问题方向相反. 如矩阵连乘问题, 最长公共子序列问题等.

证明方法

  • 贪心选择性质
    要证明第一步是正确的, 可以先假设已有最优解 A A A, 把 A A A 的第一步换成贪心算法的第一步, 其结果 A ′ A' A 不比 A A A 差.
  • 最优子结构性质
    只需要证明经过第一步选择, 问题转换成了与原问题性质完全相同的子问题.
    类似于数学归纳法.

4.3 哈夫曼编码

字符出现的频率与其权值成正比,求使得平均码长最短的编码方案
例: 字符权重为: 45 , 12 , 13 , 5 , 9 , 16 45, 12, 13, 5, 9, 16 45,12,13,5,9,16, 构造哈夫曼树.

算法设计与分析 (4. 贪心算法, 未完)_第1张图片
图 3. 哈夫曼树

完整Java 代码

时间复杂度分析

方案一 (所提供的代码使用本方案):

  • 令字符数为 n n n, 共进行 n − 1 n - 1 n1 次合并.
  • 每次需要在剩下的字符中选择两个权值最小的, 时间为 O ( n ) O(n) O(n).
  • 总时间为 O ( n 2 ) O(n^2) O(n2).
    方案二:
  • 将所有字符按权重排序, 时间 O ( n log ⁡ n ) O(n \log n) O(nlogn).
  • 每次选择两个权重最小的字符合并, 结果为一个新字符, 根据其权重插入原序列, 时间 O ( log ⁡ n ) O(\log n) O(logn).
  • 总时间为 O ( n log ⁡ n ) O(n \log n) O(nlogn).

算法正确性证明

哈夫曼树的构造过程中, 其贪心策略是使权重最小的字符对应的节点在树中层次最深. 记字符集为 Σ \Sigma Σ, 字符 x ∈ Σ x \in \Sigma xΣ 的权重为 w x w_x wx, 且 ∑ x ∈ Σ w x = 1 \sum_{x \in \Sigma} w_x = 1 xΣwx=1.
(a) 贪心选择性质. 不失一般性, 令权重最小的字符为 a a a. 现在需要证明存在哈夫曼树, 使得 a a a 对应的节点深度最大. 假设 T T T 为一棵最优二叉树, 其中节点 x x x 的层次记为 T x T_x Tx. T T T 的平均编码长度为
L ( T ) = ∑ x ∈ Σ w x T x (1) L(T) = \sum_{x \in \Sigma} w_x T_x \tag{1} L(T)=xΣwxTx(1)
令深度最大的某个节点为 b b b, 即 T b = max ⁡ x ∈ Σ T x T_b = \max_{x \in \Sigma} T_x Tb=maxxΣTx. 现将 a a a b b b 的位置互换, 获得新的二叉树 T ′ T' T. 则 T ′ T' T T T T 的编码长度差别由 a a a, b b b 两个字符确定.
L ( T ) − L ( T ′ ) = w a T a + w b T b − w a T b − w b T a = ( w a − w b ) ( T a − T b ) (2) L(T) - L(T') = w_a T_a + w_b T_b - w_a T_b - w_b T_a = (w_a - w_b) (T_a - T_b)\tag{2} L(T)L(T)=waTa+wbTbwaTbwbTa=(wawb)(TaTb)(2)
由于 w a ≤ w b w_a \leq w_b wawb, T a ≤ T b T_a \leq T_b TaTb, L ( T ) − L ( T ′ ) ≥ 0 L(T) - L(T') \geq 0 L(T)L(T)0. T ′ T' T 也一定是最优二叉树.
因此, 将权重最小的字符放到树中最深层次是一个正确的贪心选择方案.
(b) 最优子结构. 假设最深的一树节点对应的字符为 a a a b b b. 令 Σ ′ = Σ − { a , b } ∪ { z } \Sigma' = \Sigma - \{a, b\} \cup \{z\} Σ=Σ{ a,b}{ z}, 且 w z = w a + w b w_z = w_a + w_b wz=wa+wb. 需要证明将 T T T 中对应于 a a a, b b b 的叶节点删除, 并令其父节点对应的字符为 z z z, 所获得的新二叉树 T ′ T' T Σ ′ \Sigma' Σ 的最优二叉树.
(未完待续)

4.4 单源最短路径

4.5 最小生成树

4.6 小结

你可能感兴趣的:(算法设计与分析课程,算法,开发语言,线性代数)