第 16 章 贪心算法 (Greedy Algorithms)

      +

      核心结论

      • 贪心算法定义:每一步做局部最优选择,期望得到全局最优;区别于动态规划——动态规划先求解子问题再做选择,贪心算法在子问题求解之前就做出选择且不可回溯。

      • 贪心选择性质 (Greedy-Choice Property):通过局部最优(贪心)选择可以构造全局最优解;必须在求解子问题之前证明存在一个最优解包含此贪心选择。

      • 最优子结构 (Optimal Substructure):问题的最优解包含子问题的最优解;这是贪心和动态规划共同的基础。

      • 贪心不一定最优:0/1 背包问题不能用贪心求解(要用 DP),但分数背包可以;活动选择、Huffman 编码、最小生成树 (Kruskal/Prim)、单源最短路径 (Dijkstra)、集合覆盖启发式是贪心有效的经典案例。

      • 拟阵 (Matroid):当问题可形式化为加权拟阵的最大权独立子集时,贪心必最优;图的生成森林构成「图拟阵」,这解释了 MST 类算法为何有效。

      • 与分治、动态规划对比:贪心不分解为多个子问题(只一个),不依赖子问题结果,且每步不存储中间表;动态规划处理子问题重叠,贪心适用于每步唯一选择的情况。

      本章主旨

      本章是算法设计范式之一「贪心」的集中论述。5 节内容依次展开:从活动选择展示「贪心选择性质 + 最优子结构」的证明模式,到 Huffman 编码给出工程级应用,再用拟阵理论统一刻画「何时贪心有效」,最后以单位时间任务调度作收束。整章贯彻一条主线:贪心算法看起来「盲目」,但只要满足两个性质,就能严格证明最优。第 23 章 MST、第 24 章 Dijkstra、第 35 章集合覆盖启发式都建立在贪心之上——本章是为这些后续算法铺垫的。

      一、核心概念

      本章围绕 5 个核心概念展开:先建立贪心方法的两个判定条件(16.1 + 16.2),再用 Huffman 编码作应用(16.3),通过拟阵理论统一刻画适用范围(16.4),最后落实到单位时间任务调度(16.5)。

      概念 定义 + 重要性 实现提示

      贪心选择性质

      一个全局最优解包含当前贪心选择;意味着「只看眼前」不会错失最优。

      §16.1 活动选择 (定理 16.1);证明用「cut-and-paste」把贪心选项替换原最优解中的某选项。

      最优子结构

      问题最优解包含子问题最优解;与 DP 共用,本章特化为「贪心选择后只剩一个子问题」。

      §16.2 给出与 DP 的对比 (0/1 背包 vs. 分数背包);活动选择把动态规划式 16.2 化简为贪心。

      Huffman 编码

      用字符频率构造最优前缀码;按频率合并两个最小节点建立二叉树,最小化 B(T)=Σ c.freq·d(c)。

      §16.3;HUFFMAN 用最小堆 Q 选出 x、y 合并;运行时间 O(n lg n) (或 O(n lg lg n) 用 vEB 树)。

      拟阵与最大权独立集

      加权拟阵 M=(S,ℐ,w) 上贪心按权重降序、跳过破坏独立性的元素 → 必得最大权独立子集;图拟阵 M_G 给出 MST。

      §16.4 证明 (定理 16.7-16.11);χ(S)=t(S)+2m(S) 势函数;GREEDY 时间 O(n lg n + n·f(n))。

      任务调度拟阵

      单机单位时间任务 + 截止期 + 罚金的最小罚金调度,可形式化为拟阵 (S, ℐ),其中 ℐ 是可按时完成的任务集。

      §16.5 定理 16.13 给出独立性定义 Nt(A)≤t;按罚金降序贪心 + 拟阵定理 → 最优。单位优先级排序得 O(n²)。

      二、详细笔记

      2.1 活动选择问题 (Activity-Selection Problem)

      What:给定 n 个活动,每个有开始时间 \(s_i\) 和结束时间 \(f_i\)(\(s_i < f_i\),已按 \(f_i\) 升序排好),求最大规模的相互兼容(不重叠)活动子集。

      Why:活动选择是贪心算法的「教科书级例子」——动态规划给出 O(n²) 公式(式 16.2),贪心简化为 O(n);它清晰演示「DP 子问题可以删掉一半」后如何改写为贪心递归。

      How

      1. 设 \(S_k = \{a_i \in S : s_i \geq f_k\}\) 为「在 a_k 结束后开始的活动」。

      2. 贪心选择:选 \(S_k\) 中最早结束的活动 \(a_m\)(即 \(m = \min\{i : f_i \text{ 最小的 } a_i \in S_k\}\))。

      3. 最优子结构:选 \(a_m\) 后,问题化归为 \(S_m\) 的同一子问题(只需一个递归调用,而不是 DP 的 max)。

      贪心递归(伪代码):

      \[S_k 上的贪心选择 a_m ⟹ |A*| = |A*_{S_m}| + 1\]

      When:当候选解空间巨大(指数级)但每步能做「局部最优」且能严格证明该选择进入某个最优解时,活动选择这种「区间调度」即典型场景。

      Example:n=11 活动(CLRS 图 16.1)中,贪心按 \(f_i\) 选 \(\{a_1, a_4, a_8, a_{11}\}\),4 个活动为最大相容集。

      2.2 贪心策略的两个性质

      What:贪心方法 (Generic) 由两步构成——(1) 做出贪心选择;(2) 求解「剩下」的子问题;要求满足「贪心选择性质」与「最优子结构」。

      Why:贪心算法的不严格性使其容易被误用(如 0/1 背包);明确两个性质提供「贪心是否适用」的判据。第 16 章的其余小节都围绕建立这两条性质。

      How

      性质 形式化

      贪心选择性质

      对每步子问题,存在某个最优解包含当前「看似最优」的选择(无需看未来)。

      最优子结构

      问题的最优解包含子问题的最优解。

      贪心 vs. DP:

      问题 贪心? 替代

      活动选择

      是(每步最早结束)

      DP O(n²)(式 16.2)

      分数背包

      是(按 value/weight 降序)

      0/1 背包

      否(整数约束导致子问题重叠)

      DP,O(nW)

      Huffman 编码

      是(每次合并最小两频率)

      When:拿到一个优化问题时,先检验两个性质;若证明贪心性质 + 最优子结构都能成立,可用贪心得到最快的多项式解。

      Example:0/1 背包反例(CLRS 图 16.2)——物品 1(60/10=6)、物品 2(100/20=5)、物品 3(120/30=4),W=50。贪心先选物品 1 后剩 40 磅放不下物品 2+3(需 50),总价值 160;最优是物品 2+3 = 220。贪心失败因为「跳不出已选的子集」。

      2.3 Huffman 编码

      What:Huffman 编码是一种最优前缀码 (prefix code)——给定字符频率,把每个字符表示为 0/1 串,使编码长度加权和最小 \(B(T) = \sum_{c \in C} c.\text{freq} \cdot d_T(c)\)。

      Why:Huffman 是数据压缩的核心算法(典型压缩率 20%-90%)。它把「压缩比优化」转化为「带权二叉树的合并问题」,是贪心算法在工程上最重要的应用之一。

      How

      1. HUFFMAN 算法(CLRS 伪代码):

        • 初始化最小堆 Q ← C。

        • 循环 \(n-1\) 次:取 x = EXTRACT-MIN(Q),取 y = EXTRACT-MIN(Q),合并 z(z.freq = x.freq + y.freq),z.left = x,z.right = y,INSERT(Q, z)。

        • 返回 Q 中唯一节点(根)。

      2. 正确性:引理 16.2(贪心性质——最小两频率 x、y 必在最优前缀码中互为兄弟)、引理 16.3(最优子结构——合并后的子问题独立)。定理 16.4:「HUFFMAN 产生最优前缀码」。

      3. 复杂度:O(n lg n)(用二叉堆)或 O(n lg lg n)(用 van Emde Boas 树,第 20 章)。

      \[B(T) = \sum_{c \in C} c.\text{freq} \cdot d_T(c) \qquad (16.4)\]

      When:需要压缩的字符集 + 已知频率;字符数 n 不太大;解码端可拿到码表(通常发送码表只占 O(n) 比特)。

      Example:CLRS 图 16.3 中 6 字符,频率 {a=45, b=13, c=12, d=16, e=9, f=5}。定长码需 3×100000=300000 比特;Huffman 可压到 45·1+13·3+12·3+16·3+9·4+5·4 = 224000 比特。

      2.4 拟阵与贪心方法

      What:拟阵 M = (S, ℐ) 是满足 (1) 遗传性 (ℐ 在子集下封闭)、(2) 交换性 (|A|<|B| 时 ∃ x ∈ B\A 使 A∪{x} ∈ ℐ) 的序对;加权拟阵问题 = 在 ℐ 中找最大权独立集。

      Why:拟阵理论是「贪心有效」的精确刻画——图拟阵 M_G(独立集 = 无环边集)解释了为何 MST 类算法用贪心有效;任务调度拟阵解释了 §16.5 的正确性。

      How

      拟阵元素 定义 / 性质 备注

      S

      有限「元素集」

      加权问题里是边集 / 任务集

      独立子集族,满足遗传性 + 交换性

      例:图 G = (V,E) 的无环边集

      最大独立子集

      极大(不可再扩)独立子集

      定理 16.6:所有最大独立子集大小相同

      加权

      w: S → ℝ⁺

      最大权独立子集 = 拟阵贪心目标

      收缩 M/x

      移除 x 后,所有与 {x} 仍独立的子集

      引理 16.10:子问题结构

      贪心算法 (CLRS 伪代码 GREEDY):

      按 w 降序排序 → 依次试每个元素 → 加入当且仅当保持独立性。

      势函数 χ(H) = t(H) + 2·m(H) 用于复杂度分析。

      \[\text{GREEDY 总时间} = O(n \lg n + n \cdot f(n))\]

      When:当问题可建模为加权拟阵(如 MST、单位时间任务调度、最大权森林、线性代数中的向量独立)时,贪心可证明最优。

      Example:图拟阵 M_G = (E, {无环边集}),加权即为 MST 问题(贪心选最小权边 + 不形成环 = Kruskal)。

      2.5 任务调度问题 (Task Scheduling)

      What:n 个单位时间任务 \(a_i\),截止期 \(d_i\),罚金 \(w_i\);单机调度使「逾截止期任务的总罚金」最小。

      Why:看似复杂的优化问题可形式化为拟阵(定理 16.13),贪心按罚金降序即可最优——这是「先把 NP-hard / hard 问题识别出来再用特定结构」的范例。

      How

      1. 集合 A 独立(定义 16.5 节):A 中任务按截止期升序排列可全按时完成 ⟺ \(\forall t,\, N_t(A) := |\{a_i \in A : d_i \leq t\}| \leq t\)。

      2. 拟阵结构:定理 16.13 证明 (S, ℐ) 满足遗传性 + 交换性,故 GREEDY 给出最优独立集。

      3. 贪心步骤:按 \(w_i\) 降序排任务;若加入 A 后仍独立就加入;否则跳过(A 中元素即为「按时完成」子集)。

      4. 复杂度:每次独立性检查 O(n),共 O(n²);用第 21 章 disjoint-set 森林可优化(问题 16-4)。

      When:单位时间 + 单机 + 截止期 + 罚金 = 「应做」而非「应拒」的二元决策;超出范围(多机 / 非单位时间)需其他算法。

      Example:CLRS 图 16.7 — 7 任务,贪心选 {a1(70),a2(60),a3(50),a4(40),a7(10)} 按时完成,剩余 {a5, a6} 罚金 50。最优调度 ⟨a2,a4,a1,a3,a7,a5,a6⟩ 总罚金 50。

      三、关键图表

      核心公式对照表
      公式 含义

      式 16.1

      活动按 \(f_i\) 单调升序:\(f_1 \leq f_2 \leq \cdots \leq f_n\)

      式 16.2

      活动选择 DP 递推(最后用不到):\(c[i,j\) = \max_{a_k \in S_{ij}}(c[i,k] + c[k,j] + 1)]

      式 16.3

      贪心迭代中 \(f_k = \max\{f_i : a_i \in A\}\)

      式 16.4

      Huffman 树代价:\(B(T) = \sum_{c \in C} c.\text{freq} \cdot d_T(c)\)

      拟阵律

      遗传性 + 交换性

      势 χ

      \(\chi(H) = t(H) + 2 m(H)\)

      任务独立

      latexmath:[N_t(A) :=

      \{a_i \in A : d_i \leq t\}

      \leq t]

      四、思维导图

      mindmap
        root((第 16 章 贪心算法))
          贪心选择性质
            局部即全局
            cut-and-paste 证法
          最优子结构
            与DP共用
            只剩一个子问题
          活动选择
            按f_i排序
            最早结束优先
          Huffman编码
            最小堆合并
            前缀码
          拟阵理论
            遗传性交换性
            图拟阵M_G
            最大权独立集
          任务调度
            单位时间任务
            截止期罚金
            罚金降序贪心
          与DP分治对比
            DP重叠子问题
            分治独立子问题
            贪心唯一选择

      五、重点与易错点

      1. 贪心 vs. DP:贪心在「子问题求解之前」做选择;DP 是先解子问题再选。若每步只有 1 个子问题 ⟹ 通常贪心快;若有多子问题重叠 ⟹ DP。

      2. 0/1 背包不能用贪心,分数背包可以——经典反例:取最大 value/weight 可能占满空间,反而失去可容纳的大件。

      3. 证明贪心算法要证 2 件事:(1) 存在某个最优解含贪心选择(cut-and-paste);(2) 剩下的问题仍具最优子结构。两者缺一不可。

      4. Huffman 必须用最小堆合并最大两频率吗?——最小两频率合并;引理 16.2 是「存在最优码」使最小两频率互为兄弟,因此合并无妨。

      5. 拟阵并不覆盖所有贪心问题——活动选择和 Huffman 都不构成拟阵(活动选择:独立集 = 任何兼容子集;MST 和任务调度才是)。

      6. 任务调度中「独立性」不是「两两不冲突」,而是「存在按时完成的次序」——式 16.5 节中表述为 \(N_t(A) \leq t\)。

      7. 贪心算法的代价:找到「贪心选择后只剩一个子问题」的等式,并说明其规模严格变小——否则会因子问题不缩而失败。

      8. Kruskal 算法在加权图、无负权、连通假设下总能给出 MST;若有权为负,则需变号处理或加常数偏移(参见 §16.4)。

      9. 复杂度统计:Huffman 用二叉堆是 O(n lg n);用 Fibonacci 堆仍是 O(n lg n) 但常数大;用 van Emde Boas 树可改进到 O(n lg lg n)。

      10. 集合覆盖的贪心(Chvátal 算法,见第 35 章)是「贪心给出 \(H_n\) 近似」的经典案例;本章强调「贪心最优」而第 35 章讨论「贪心近似」。

      11. 在 CLRS 第 4 章分治、第 15 章 DP、第 16 章贪心、第 17 章摊还之间,贪心是最「脆弱」但通常最「快」的——必须先证两个性质再使用。

      12. 引理 16.2 的交换论证(cut-and-paste):取任意最优树 T,把最大深的兄弟 (a,b) 与最小频率 (x,y) 交换;因 x.freq ≤ a.freq、y.freq ≤ b.freq,交换不增代价,故最优码可在「x,y 兄弟」中选取。