算法设计与分析 (4. 贪心算法, 含正确性证明)

本文介绍了贪心算法的基本思想和应用,通过活动安排问题和找零钱问题举例说明。贪心算法在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的。文章还探讨了贪心选择性质和最优子结构,并以哈夫曼编码为例证明了贪心算法的正确性。此外,提到了贪心算法在单源最短路径和最小生成树问题中的应用。
摘要由CSDN通过智能技术生成

贪心算法的基本思想

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

分析

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

4.1 活动安排问题

问题提出

公司不同职员申请使用某一会议室, 他们的活动起止时间如向量 s s s f f f 所示. 例如, 第 1 个活动预计 1 点至 5 点, 第 2 个活动预计 2 点至 3 点. 如何安排, 使得该会议室可以容纳最大数量的活动?
s = [ 0 , 1 , 2 , 3 , 3 , 5 , 5 , 6 , 8 , 8 , 12 ] s = [0, 1, 2, 3, 3, 5, 5, 6, 8, 8, 12] s=[0,1,2,3,3,5,5,6,8,8,12]
f = [ 6 , 4 , 13 , 5 , 8 , 7 , 9 , 10 , 11 , 12 , 14 ] f = [6, 4, 13, 5, 8, 7, 9, 10, 11, 12, 14] f=[6,4,13,5,8,7,9,10,11,12,14]

解决方案

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

i i i1234567891011
s [ i ] s[i] s[i]130535688212
f [ i ] f[i] f[i]4567891011121314

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

/**
 * Title:        活动安排问题.<br>
 * Version:      1.0<br>
 * Copyright:    Copyright (c) 2003, all rights reserved<br>
 * Author:       闵帆<br>
 * Company:      <a href=http://www.fansmale.com>www.fansmale.com</a><br>
 * Written time: 2003/09/02<br>
 * Last modify time: 2021/11/30<br>
 */
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] < f[a_2] < \dots < 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, 构造哈夫曼树.

图 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) 最优子结构. 不失一般性, 令某棵最优二叉树 T T T 中, 最深的一对节点对应的字符为 a a a b b b. 令 Σ ′ = Σ ∖ { a , b } ∪ { z } \Sigma' = \Sigma \setminus \{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 节点删除之后的二叉树为 T 1 T_1 T1, 其中 a a a 所对应的原父节点对应于新字符 z z z. 需要证明 T 1 T_1 T1 Σ ′ \Sigma' Σ 的最优二叉树.
假设 Σ ′ \Sigma' Σ 的最优二叉树不是 T 1 T_1 T1, 则 ∃ \exists Σ ′ \Sigma' Σ 的另一棵二叉树 T 2 T_2 T2, s.t. L ( T 2 ) < L ( T 1 ) L(T_2) < L(T_1) L(T2)<L(T1). 将 T 2 T_2 T2 的叶节点 z z z 扩展成 a a a b b b 获得 T 3 T_3 T3, 则 T 3 T_3 T3 为原字母表 Σ \Sigma Σ 对应的一棵二叉树. 由于 a a a b b b 的编码长度比 z z z 的多 1, L ( T 3 ) = L ( T 2 ) + w a + w b < L ( T 1 ) + w a + w b = L ( T ) , L(T_3) = L(T_2) + w_a + w_b < L(T_1) + w_a + w_b = L(T), L(T3)=L(T2)+wa+wb<L(T1)+wa+wb=L(T), 这与 T T T 为最优二叉树的假设矛盾.
综上所述, (a) 证明了贪心选择性质, (b) 证明了最优子结构. 因此算法的正确性得证.

4.4 单源最短路径

练习: 证明算法的正确性

4.5 最小生成树

4.5.1 Prim 算法

证明:令无向图为 G = ( V , E ) G = (\mathbf{V}, \mathbf{E}) G=(V,E), 其中 V = { v 1 , v 2 , … , v n } \mathbf{V} = \{v_1, v_2, \dots, v_n\} V={v1,v2,,vn}, E = { e 1 , e 2 , … , e m } \mathbf{E} = \{e_1, e_2, \dots, e_m\} E={e1,e2,,em}. 其生成树为 G ′ = ( V , E ′ ) G' = (\mathbf{V}, \mathbf{E}') G=(V,E), 其中 E ′ = { e t 1 , e t 2 , … , e t n − 1 } \mathbf{E}' = \{e_{t_1}, e_{t_2}, \dots, e_{t_{n-1}}\} E={et1,et2,,etn1}, 且 G ′ G' G 为联通无环图.
(贪心选择性质)
不失一般性, 令起始节点为 v 1 v_1 v1, 贪心选择策略是找到与 v 1 v_1 v1连接边最短的节点, 以及相应的边. 记该点为 v a v_a va, 相应的边为 e b = ( v 1 , v a ) e_b = (v_1, v_a) eb=(v1,va). 现证明 e b e_b eb 包括在某个最优解中.
G ′ G' G 为一棵最小生成树, 其中不包括 e b e_b eb. 则将 e b e_b eb 加入 G ′ G' G 中, 将形成一个环. 不考虑该环之外的节点, 每个节点的度刚好为 2. 令该环中与 v 1 v_1 v1 相关的另一条边为 e c e_c ec, 则删除 e c e_c ec 后, 获得另一棵生成树 G ′ ′ G'' G′′. 由于 w ( e b ) ≤ w ( e c ) w(e_b) \le w(e_c) w(eb)w(ec), G ′ ′ G'' G′′ 也一定为一棵最小生成树.
(最优子结构性质)

4.6 小结

算法正确性证明有一定的套路, 但写清楚并不容易.

"1402做菜顺序"这个表述似乎不完整,可能是想问的是“如何使用贪心算法来确定最优的做菜顺序”。在烹饪或任务调度中,贪心算法可以用来优化步骤安排,通常是在每一步中选择当前看起来最好的或者最有利的选择,希望这些局部最优解最终汇聚成全局最优解。 举个例子,假设你有若干道菜需要准备,每道菜都有开始和结束时间,贪心算法的步骤可能如下: 1. **初始化**:收集所有菜品的信息,如开始时间(start_time)和结束时间(end_time)。 2. **排序**:按照结束时间升序排列菜品,因为你想尽快完成那些用时短的菜,以便腾出时间做其他菜品。 3. **选择**:从时间上最早的菜品开始,按照这个顺序依次做菜。 4. **贪心决策**:在每一步都选择剩余菜品中最早完成的那道。 **贪心证明**(如果这是你要的)通常是通过构造一个满足贪心策略的最优解,并证明无论初始状态如何,只要遵循这个策略,最终结果都是最优的。然而,贪心算法并不总是能得到全局最优解,比如某些情况下可能存在依赖关系(即一道菜必须在另一道菜完成后才能开始),这时就需要更复杂的策略,如动态规划。 如果你对特定的数学证明或者烹饪中的贪心算法应用有兴趣,可以提供更详细的问题,我会给出更精确的解释和证明方法。相关问题: 1. 你能具体描述一个烹饪场景下的贪心算法例子吗? 2. 如何证明这个做菜顺序是局部最优的? 3. 哪些情况下贪心算法的做菜顺序不是全局最优?
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值