第 14 章 贪心

本章解决的问题:每一步都挑眼前最好的那个,什么时候能保证全局最优?以及它悄悄出错的时候,为什么连个报错都没有?

14.1 贪心的适用条件

学习目标:学完本节,你能

  • 说出贪心算法的定义,并指出它和"穷举"的本质区别;
  • 说清贪心成立的两个前提,以及哪一个才是贪心独有的;
  • 用一句"一条路 vs 所有路"说清贪心与动态规划的关系
  • 对一个新的问题判断该不该用贪心,并知道怎么验证自己的判断。

先修:13.3(Dijkstra 是一种贪心)、13.4(负权反例)。 固定术语:贪心算法(Greedy Algorithm)、最优子结构、贪心选择性质。 环境与版本:.NET 8 / C# 12。 预计阅读:28 分钟。


一、直觉:只看眼前一步,凭什么能赢

先看一个日常问题

收银员找零 63 元,手里有 50、20、10、5、1 元的纸币/硬币,怎么找张数最少?

绝大多数人的做法(不用想):

  63 -> 先拿 50(还剩 13)
     -> 再拿 10(还剩 3)
     -> 再拿 1、1、1(还剩 0)
  一共 5 张

这个做法叫"贪心"

贪心算法(Greedy Algorithm)每一步都在"当前看起来最好"的选项里挑一个,选完之后不回头、不反悔。

"当前看起来最好" 就是"拿不超过剩余金额的最大面额"。这个策略不用规划、不用回溯、不用记住任何历史 —— 每一步只看眼前。

小陈第一次听说"贪心"这个术语时觉得奇怪 —— "只看眼前"听起来像一种缺点,凭什么能保证答案是对的?

这一节就回答这个问题。而且答案可能让他意外:

在本节的实测里,随机生成的 1000 种币制中,这个贪心策略只有 2.5% 能保证全对。

"只看眼前"确实是一种缺点 —— 只是我们平时用的币制恰好是那 2.5%。


二、形式化:两个前提,其中一个才是关键

一个问题能不能用贪心,取决于它是否满足两个前提。

前提一:最优子结构(Optimal Substructure)

问题的最优解,一定由"子问题的最优解"拼成。

找零问题满足它:如果"凑出 63 元的最优解"里第一个拿了 50,那剩下的 13 元也必须是"凑出 13 元的最优解" —— 否则把 13 元那段换掉,整个 63 元的解会更好,与"它是最优的"矛盾。

⚠️ 注意:这个前提【不是贪心独有的】。

动态规划(第 15 章)同样需要它 —— 它是"问题能递归求解"的基础,几乎所有算法都建立在它上面。

前提二:贪心选择性质(Greedy Choice Property)

每一步的那个"局部最优选择",一定属于某个全局最优解。

换句话说你不需要"回头看"—— 当前这个最优选择永远不会让你后悔。

这才是贪心独有的前提,也是它能不能用的分水岭。

找零问题满足吗?

币制 满足贪心选择性质吗
1, 5, 10, 25(美元) ✅ 满足 —— 拿最大的总是安全的
1, 3, 4(只是换了面额) 不满足 —— 下面实测给你看

同一个问题、同一份代码,换个币制就从"对"变成"错"。

与动态规划的关系:一条路 vs 所有路

这是本节最该记住的一句话

贪心是动态规划的一个"退化特例" —— 当你确信"每个状态只需要保留一个候选"时,DP 就缩成了贪心。

用代码对照最清楚

// 贪心:每个状态只走【一条路】
int rest = amount, count = 0;
foreach (int c in coins)              // coins 按降序
    while (rest >= c) { rest -= c; count++; }
// 动态规划:每个状态尝试【所有路】,取最好的
for (int a = 1; a <= amount; a++)
    foreach (int c in coins)
        if (c <= a && dp[a - c] + 1 < dp[a])
            dp[a] = dp[a - c] + 1;

看那个内层

每个状态尝试多少种选择 需要保存什么
贪心 1 种(最大的那个) 一个剩余金额变量
动态规划 所有面额 一整张表($amount$ 个状态)

贪心快,因为它把"只有一条路"这件事当成事实。 DP 慢,因为它老老实实把每条路都试了一遍。

"哪条路是对的"就是贪心选择性质要回答的问题。


三、实测一:同一份代码,换个币制就从对变错

写一份贪心找零,先拿美元币制试

  币制 A:1, 5, 10, 25(美元的币制)
    面额(降序):25, 10, 5, 1

      金额    贪心枚数    最优枚数    一致
      ----    --------    --------    ----
         1           1           1    ✓
         2           2           2    ✓
         3           3           3    ✓
         4           4           4    ✓
         5           1           1    ✓
         6           2           2    ✓
         7           3           3    ✓

    1 ~ 30 共 30 个金额,贪心【全部正确】✓

然后把面额换成 1, 3, 4 —— 代码一个字没改

  币制 B:1, 3, 4(只换了面额,代码一个字没改)
    面额(降序):4, 3, 1

      金额    贪心枚数    最优枚数    一致
      ----    --------    --------    ----
         1           1           1    ✓
         2           2           2    ✓
         3           1           1    ✓
         4           1           1    ✓
         5           2           2    ✓
         6           3           2    ✗   <- 贪心拿的是 4 + 1 + 1
         7           2           2    ✓
        10           4           3    ✗   <- 贪心拿的是 4 + 4 + 1 + 1
        14           5           4    ✗   <- 贪心拿的是 4 + 4 + 4 + 1 + 1
        18           6           5    ✗   <- 贪心拿的是 4 + 4 + 4 + 4 + 1 + 1
        22           7           6    ✗   <- 贪心拿的是 4 + 4 + 4 + 4 + 4 + 1 + 1
        26           8           7    ✗   <- 贪心拿的是 4 + 4 + 4 + 4 + 4 + 4 + 1 + 1
        30           9           8    ✗   <- 贪心拿的是 4 + 4 + 4 + 4 + 4 + 4 + 4 + 1 + 1

    1 ~ 30 共 30 个金额,贪心有 7 个出错(第一个是 6)

把第一个出错的金额展开看

    金额 = 6
      贪心:4 + 1 + 1  ->  3 枚
      最优:3 + 3      ->  2 枚
      贪心错在第一步:拿了 4,而最优解根本不碰 4。

这就是贪心选择性质被打破的样子

拿 4 在"当下"看起来是最好的(它让剩余金额从 6 降到 2,一步就消掉最大的一块), 但它堵死了后面"两个 3"这条路。

而这个错误在美元币制下永远不会发生 —— 因为 5 = 5×110 = 2×525 = 5×5每个面额都是更小面额的"整倍数",所以"拿大的"永远不亏。

注意出错金额的规律(6, 10, 14, 18, 22, 26, 30)—— 从 10 开始每 4 个错一次, 错在"离 4 的倍数差 2"的位置上。贪心的错误是有规律的,但规律本身也是错的。


四、实测二:随机币制里,贪心能用的只有 2.5%

一个币制对了不代表什么 —— 因为我们平时用的币制是"被人设计过的"。

如果把面额随机化呢?实测(1000 种随机币制,每种含面额 1,另加 4 个 2~30 的随机面额)

  对每种币制,检查金额 1 ~ 200 贪心与最优解是否全部一致:

    贪心【全部正确】的币制:25 种(2.5%)
    贪心【至少错一次】的币制:975 种(97.5%)

  几个反例(面额已按降序排列):
    币制 {12, 9, 7, 5, 1},金额 14:贪心 3 枚,最优 2 枚
    币制 {30, 28, 23, 12, 1},金额 35:贪心 6 枚,最优 2 枚
    币制 {28, 25, 5, 2, 1},金额 50:贪心 6 枚,最优 2 枚

25 / 1000 —— 只有 2.5%。

这个数字值得停下来想一想

"每次拿最大的"这个策略,在绝大多数币制下都是错的。

我们觉得它"显然对",是因为人民币和美元的面额都被设计成了"大面额是小面额的整倍数" —— 那是货币设计的结果,不是贪心的功劳。

更要命的是第三条反例

    币制 {28, 25, 5, 2, 1},金额 50:贪心 6 枚,最优 2 枚

贪心要 6 枚(28 + 5 + 5 + 5 + 5 + 2),最优只要 2 枚(25 + 25 —— 差了 3 倍

而贪心给出的答案看起来"很合理" —— 它每一步都拿了当前能拿的最大面额,你按它的思路走一遍,找不出哪里不对。

这就是贪心最危险的地方:它错了,但它错得让人看不出来。


五、实测三:贪心 vs 动态规划

把同一个问题的 DP 解法也写出来,对照着看

  币制 B(1, 3, 4)上,任取两个金额:
    金额   6:贪心 =   3 枚,动态规划 =   2 枚,一致 = False
    金额 300:贪心 =  75 枚,动态规划 =  75 枚,一致 = True

注意第二行金额 300 时贪心没错。

贪心的问题是"有时错",不是"总是错" —— 而这恰恰是最危险的:它不会每次都露馅。

一个总出错的算法很快会被发现;一个 2.5% 情况下正确的算法,配上"手边这个币制刚好是对的", 就会一路通过测试、上线、直到某个用户输入了一个奇怪的金额。

再看性能(币制 200,100,50,25,10,5,1,金额 200,000,各跑 1000 次):

    贪心      :     1.9 ms   (结果 1000 枚)
    动态规划:   782.1 ms   (结果 1000 枚)
    动态规划是贪心的 403 倍耗时

复杂度上也对得上

复杂度 与金额的关系
贪心 $O(k)$,$k$ 是面额种数 无关 —— 金额涨 10 倍,耗时不变
动态规划 $O(amount \times k)$ 线性 —— 金额涨 10 倍,耗时涨 10 倍

403 倍这样的差距,就是"贪心为什么值得研究"的全部理由。

但这一节的结论不是"用 DP 更保险" ——

而是:能用贪心时就用贪心,但你必须能【证明】它在这里是对的。


六、怎么确认一个贪心是对的

"证明"这件事听起来很重,实际有两条路可走。

路径一:交换论证(Exchange Argument)

这是证明贪心正确性的标准手法,模板是固定的四步

  1. 假设存在一个最优解 O
  2. 找到 O 和贪心解 G 的【第一个不同的选择】
  3. 把 O 的那个选择换成 G 的选择,证明【结果不会变差】
  4. 反复交换,O 就被一步步改造成了 G
     -> 每一步都没变差,所以 G 也是最优点 ✓

它的直觉是"贪心的选择"和"最优解的选择"可以互换,而互换不吃亏 —— 这就是贪心选择性质的另一种说法。

14.2 节讲区间调度时会完整走一遍这四步 —— 那里会看到它实际用起来是什么样子。

路径二:小规模对拍(工程上更常用)

如果你证不出来,但想先用 —— 那就用暴力/DP 在小规模上对拍

本节实验一、实验二做的就是这件事

  对每种币制,检查金额 1 ~ 200 贪心与最优解是否全部一致

这是本书反复出现的手法(12.3 节的 240 组、13.2 节的 240 组、13.3 节的 225 个顶点):

不要自己验证自己 —— 写一个思路完全不同的实现(DP、暴力枚举、Floyd-Warshall), 在小规模上跑一批数据,看两者是否处处一致。

验完之后你得到的是"经验证据",不是"证明" —— 但对工程来说,小规模全对 + 大规模抽样对,通常已经够了

两条路径的取舍

交换论证 小规模对拍
给出的是 数学证明 经验证据
成本 高(要想清楚) 低(写个 DP 就行)
什么场景用 写论文、做关键系统、面试被追问"为什么对"时 日常开发、原型验证
风险 证错了自己不知道 没测到的输入仍可能出错

⚠️ 对拍的边界必须说清楚它只能证明"这些输入上是对的",不能证明"所有输入上都是对的"。

实验二的 2.5% 就是这个风险的量化 —— 如果当初只测了美元币制(那 30 个金额全对), 你会得出"贪心找零是对的"这个结论,然后把它用到别的币制上。

已经有人证过的贪心,直接用

工程里真正高频的贪心,其实就那么几个 —— 它们都有现成的正确性证明

问题 贪心策略 出处
最短路(非负权) 每次取距离最小的顶点 13.3 节 Dijkstra
拓扑排序 每次取任意一个入度为 0 的顶点 13.1 节 Kahn
区间调度 每次选结束最早的 14.2 节
Huffman 编码 每次合并频率最小的两个 14.2 节
最小生成树 每次加最短的安全边 超出本书范围

其余的贪心,默认按"没证过"处理。


七、练习

练习 14.1.1(找零的手工推演) 币制 {1, 3, 4},金额 6、10、14: (a) 手工跑一遍贪心(每次拿不超过剩余的最大面额),写出每步拿了什么。 (b) 用 DP 的思想算出最优解(提示:dp[6] = min(dp[5], dp[3], dp[2]) + 1)。 (c) 为什么 dp[6] 要看 dp[5]dp[3]dp[2] 这三项?

练习 14.1.2(判断) 判断对错并说明理由: (a) 满足最优子结构的问题就能用贪心。 (b) 贪心每步只保留一个候选,动态规划保留多个。 (c) 只要贪心在小规模数据上全部正确,就可以放心用到生产环境。 (d) 贪心算法一定比动态规划快。

练习 14.1.3(设计一个反例) 币制 {1, 6, 10}: (a) 贪心找零 12 元会怎么走?最优解是什么? (b) 金额 18 呢? (c) 这个币制里,贪心是"偶尔错"还是"大面积错"?

练习 14.1.4(工程判断) 小陈要做一个"给用户推荐优惠券组合"的功能:用户有一张订单(金额固定),系统要从券库里选几张券叠加,使总优惠最大(每张券有面额,且有"满 X 减 Y"的门槛)。

(a) 这个问题能用"每次选面额最大的券"这个贪心吗?先别急着答 —— 说说你会怎么验证。 (b) "满减券"这个门槛的存在,破坏了哪个前提? (c) 如果你只有一天时间上线,你会怎么做?


八、练习答案

14.1.1

(a) 贪心推演(币制 {4, 3, 1},按降序)

金额 贪心的每一步 枚数
6 拿 4(剩 2)→ 4 太大,拿 1(剩 1)→ 拿 1(剩 0) 3 枚4 + 1 + 1
10 拿 4(剩 6)→ 拿 4(剩 2)→ 拿 1(剩 1)→ 拿 1(剩 0) 4 枚4 + 4 + 1 + 1
14 4(剩 10)→ 4(剩 6)→ 4(剩 2)→ 1(剩 1)→ 1(剩 0) 5 枚4 + 4 + 4 + 1 + 1

(b) 最优解

金额 最优 枚数
6 3 + 3 2 枚
10 3 + 3 + 4 3 枚
14 4 + 4 + 3 + 3 4 枚

验证 $dp[6]$

$$dp[6] = \min(dp[6-4], dp[6-3], dp[6-1]) + 1 = \min(dp[2], dp[3], dp[5]) + 1$$

  • $dp[2] = 2$(1+1
  • $dp[3] = 1$(3
  • $dp[5] = 2$(4+1

$$\min(2, 1, 2) + 1 = 1 + 1 = 2$$

所以 $dp[6] = 2$ ✓ 对应"先拿 3,再拿 3"。

(c) 因为三个面额分别是 4、3、1。

$dp[6]$ 表示"凑出 6 元的最少枚数"。它的最后一步,必然是从某个更小的金额 $6-c$ 加上一枚面额 $c$ 得到的而 $c$ 只能是 4、3、1 中的一个 —— 所以要看 $dp[2]$、$dp[3]$、$dp[5]$ 这三项。

这正是 DP 和贪心的分界

DP 会把三项都算出来再取最小;贪心只算 $dp[2]$(对应拿 4)那一项。

贪心"省掉"的,恰好就是那个更优的 $dp[3]$。

14.1.2

小题 判断 理由
(a) 最优子结构是"必要条件"不是"充分条件"。 动态规划同样需要它,但 DP 不需要贪心选择性质。找零的币制 {1,3,4} 就满足最优子结构,但贪心是错的。
(b) 这就是"一条路 vs 所有路"。贪心每步只保留一个候选(拿最大的),DP 把每个状态的所有来源都算一遍再取最优。
(c) 小规模正确不等于全部正确。 本节实验二的 2.5% 就是这个风险的量化:如果只测了几个金额,很容易误判。除非你把规模推到能覆盖所有可能的输入,否则拿到的只是经验证据。
(d) 复杂度上贪心通常更低(本节实测 403 倍),但如果贪心是错的,比较速度就没有意义。而且有些问题的贪心版本反而更慢(比如需要额外排序)。

(c) 是本节最该记住的一条 —— 它和 13.4 节"Bellman-Ford 在随机图上反而更快"是同一类教训: "在我测过的数据上没问题"和"没问题"之间,隔着一段距离。

14.1.3

币制 {10, 6, 1}(降序)。

(a) 金额 12:贪心拿 10 + 1 + 1(3 枚),最优是 6 + 6(2 枚)。

贪心推演:12 ≥ 10 → 拿 10(剩 2);2 < 6 → 跳过 6;2 ≥ 1 → 拿 1(剩 1)→ 拿 1(剩 0)。3 枚

6 + 6 = 12 只要 2 枚

(b) 金额 18:贪心拿 10 + 6 + 1 + 1(4 枚),最优是 6 + 6 + 6(3 枚)。

贪心:18 ≥ 10 → 拿 10(剩 8);8 ≥ 6 → 拿 6(剩 2);2 ≥ 1 → 拿 1、拿 1。4 枚

最优6 + 6 + 6 = 3 枚

(c) 大面积错 —— 凡是"离 6 的倍数差一点"的金额都会错。

规律:贪心拿了一个 10 之后,剩下的金额就不好用 6 凑了

金额 贪心 最优 一致
6 6(1 枚) 6(1 枚)
12 10+1+1(3 枚) 6+6(2 枚)
18 10+6+1+1(4 枚) 6+6+6(3 枚)
24 10+10+1+1+1+1(6 枚) 6+6+6+6(4 枚)
30 10+10+10(3 枚) 10+10+10(3 枚)

具体差多少可以用"每枚 10 损失多少"来估一个 10 元换成 6+1+1+1+1(5 枚)才能凑齐,所以每拿一个 10,贪心大约多花 4 枚。

这道题和实验一那个 {1,3,4} 的差别在于{1,3,4} 错在"4 和 3 的关系", {1,6,10} 错在"10 和 6 互不整除" —— 后者错得更频繁。

14.1.4

(a) 不能直接下结论 —— 而且"每次选面额最大的"几乎肯定不对。

我会这样验证(按成本从低到高):

步骤 做法
1. 先想清楚"最优子结构"是否成立 如果用户有三张券 A、B、C,最优组合里去掉一张,剩下的是不是"剩余券里的最优组合"?如果券的门槛互相影响,这一步就未必成立
2. 写一个暴力枚举 券的数量不多时(比如 ≤ 20 张),枚举所有子集就能拿到真值
3. 小规模对拍 随机生成一批订单和券库,贪心 vs 暴力逐一对比
4. 如果发现反例 找到第一个反例,看它错在哪个假设上

这正是本节实验一、二的做法 —— 只不过那里的"暴力"换成了 DP。

(b) "满减门槛"破坏了【贪心选择性质】。

为什么?

没有门槛时(每张券都是"直接减 Y"),总优惠就是各券面额之和 —— 那么"选面额最大的 k 张"显然是最优的,贪心选择性质成立 ✓

有了门槛之后,"选哪张券"和"当前订单金额"耦合起来了

  • 一张 20 元的券门槛是"满 100",现在订单 95 元,你用了它就用不了
  • 但如果先配一张"满 50 减 5"的券把金额降到 90,那张 20 元的券连门槛都够不着了

每一步的"最优选择"依赖于当前的金额状态,而当前金额又依赖于之前每一步的选择 —— 这就是"局部最优推不出全局最优"的典型形态。

注意:这【不影响】最优子结构(去掉最后一张券,剩下的仍然是一个同样形式的子问题)—— 破的是贪心选择性质。 这正是本节第二节强调"两个前提要分清"的原因。

(c) 一天时间,我会按这个顺序做:

优先级 做法 理由
1 先做"只选一张券" 一张券的最优 = 遍历一遍取最大,永远不会错,而且覆盖大多数场景
2 叠券先用暴力枚举(限制最多叠加 3~5 张) 组合数 $C(n,3)$ 在 $n \le 50$ 时只有 2 万,毫秒级
3 如果券很多,用 DP 做 按订单金额做状态(第 15 章的背包模型)
4 绝不先上贪心 贪心错了不会报错,只会悄悄少减几块钱 —— 用户会发现的

最后一条是关键贪心的错误是"静默的"

一个崩溃的 bug 一小时内就会被告警抓到;一个"少算了 3 块钱优惠"的贪心 bug, 可能要等某个用户在微博上晒出对比截图才会被发现。

这就是为什么"能用贪心"和"能证明贪心能用"是两件必须分开的事。


九、常见错误

误区 纠正
认为"满足最优子结构就能用贪心" 最优子结构 DP 也需要,它是必要条件。贪心独有、且真正卡人的是【贪心选择性质】。
认为贪心"偶尔错一点,问题不大" 实测反例:币制 {28,25,5,2,1}、金额 50,贪心 6 枚 vs 最优 2 枚 —— 差 3 倍。 而且错误是静默的,不会报错。
在少数几个输入上验过就上线 实测:随机 1000 种币制里只有 25 种(2.5%)贪心全对。 只测美元币制(30 个金额全对)会得出完全错误的结论。
认为"贪心比 DP 快,所以优先用贪心" 贪心错了的话,快没有意义。 正确顺序是:先确认它对,再比较速度
把"贪心"和"启发式"混为一谈 贪心(能证明最优的)和启发式(只求近似解)是两回事。 很多"看起来像贪心"的策略其实是启发式,它们不保证最优
认为币制找零的贪心"数学上就是对的" 只对"规范币制"成立(每个面额是更小面额的整倍数)。人民币、美元恰好是,但这是一种设计,不是普遍规律。

十、本节总结

  1. 贪心算法:每一步都在"当前看起来最好"的选项里挑一个,选完不回头、不反悔
  2. 两个前提
    • 最优子结构 —— 最优解由子问题的最优解拼成。DP 也需要它,不是贪心独有。
    • 贪心选择性质 —— 局部最优选择一定属于某个全局最优解。这才是贪心独有、也真正卡人的那一条。
  3. 贪心与 DP 的关系(本节最重要的一句话)贪心是 DP 的退化特例 —— 贪心每个状态只走一条路,DP 走所有路。 实测性能差 403 倍(1.9 ms vs 782.1 ms)。
  4. 实测一:同一份找零代码,美元币制下 30 个金额全对,换成 {1,3,4} 30 个里错 7 个(第一个是 6 元:贪心 4+1+1 三枚,最优 3+3 两枚)。
  5. 实测二(本节最有力的反预期):随机 1000 种币制中,贪心只有 25 种(2.5%)全对我们觉得它"显然对",是因为人民币/美元的面额被设计成了整倍数关系。
  6. 贪心最危险的地方它"有时错"而不是"总是错" —— 实测金额 6 错、金额 300 对。错误是静默的,不会报错。
  7. 验证贪心的两条路径交换论证(数学证明,14.2 节会完整走一遍四步)和小规模对拍(工程常用,但只给经验证据)。
  8. 工程原则能用贪心时就用贪心,但你必须能证明它在这里是对的。 已经有人证过的(Dijkstra、Kahn、区间调度、Huffman)直接用;其余的默认按"没证过"处理。

下一节衔接:本节讲了"贪心要满足两个前提",但没有真正证明过任何一个贪心是对的 —— 交换论证的四步只给了模板。

14.2 节会拿两个经典问题把这件事做实

  • 区间调度("最多能参加几个不冲突的会议")—— 完整走一遍交换论证
  • Huffman 编码 —— 一个"看起来完全不像贪心"的贪心

而 14.3 节会反过来构造贪心失效的反例,并给出"该转向 DP 了"的信号。


状态外显

{
  "book": "数据结构与算法自学:从工程直觉到复杂度思维",
  "section": "14.1",
  "title": "贪心的适用条件",
  "covered": [
    "贪心算法的定义:每步取当前最优、不回头不反悔",
    "两个前提的分工:最优子结构(DP 也要)vs 贪心选择性质(贪心独有)",
    "「贪心是 DP 的退化特例」:一条路 vs 所有路,含代码对照",
    "实测一:美元币制 30 个金额全对,换成 {1,3,4} 则有 7 个出错(第一个是 6 元)",
    "实测二(核心反预期):随机 1000 种币制中贪心只有 2.5% 全对",
    "实测三:贪心与 DP 的正确性对照 + 403 倍性能差距",
    "交换论证的四步模板(为 14.2 节埋线)",
    "小规模对拍的方法与其边界(只给经验证据,不是证明)",
    "已证过的贪心清单与「其余默认没证过」的工程原则"
  ],
  "unresolved": [
    "交换论证的实际演练留到 14.2",
    "区间调度与 Huffman 编码留到 14.2",
    "0/1 背包等贪心失效反例留到 14.3",
    "动态规划的系统讲法留到第 15 章"
  ],
  "canonical_terms": {
    "贪心算法(Greedy Algorithm)": "每一步都取当前看起来最优的选项,选完不回头不反悔",
    "最优子结构(Optimal Substructure)": "问题的最优解由子问题的最优解拼成;DP 与贪心都需要",
    "贪心选择性质(Greedy Choice Property)": "每一步的局部最优选择一定属于某个全局最优解;贪心独有"
  },
  "symbols_units": {
    "k": "面额种数",
    "amount": "要找零的金额",
    "dp[a]": "凑出金额 a 所需的最少硬币数"
  },
  "assumptions": [
    "读者已掌握 13.3 的 Dijkstra(作为「贪心正确」的正例)",
    "读者理解 13.3 的负权反例(作为「贪心失效」的先声)",
    "币制的贪心结论只对「规范币制」成立,正文已明确说明"
  ],
  "word_count_actual": 3541,
  "checks": [
    "C# 代码已在 .NET 8 (SDK 10.0.303) Release 下实跑(AMD Ryzen 7 9700X),输出见正文",
    "项目文件:99-tools/samples/Ch14/Sec141/",
    "实验一:美元币制 1~30 全对、{1,3,4} 有 7 个出错(第一个是 6),为实测",
    "实验二:1000 种随机币制(seed=20260918)中 25 种全对、975 种有错,为实测;对拍范围为金额 1~200",
    "实验二的三条反例、实验一的 6 元展开(4+1+1 vs 3+3)均为实测",
    "实验三:金额 6 不一致、金额 300 一致;性能 1.9ms vs 782.1ms(各跑 1000 次),为实测",
    "术语写法与 glossary.md 一致(贪心算法、最优子结构、贪心选择性质)"
  ],
  "known_issues": [
    "初版表格用「硬币展开式」(如 4 + 4 + 4 + 4 + 1 + 1)作列内容,长串把表格挤歪了 —— 改为「枚数」列 + 不一致时在行尾附展开式",
    "性能对比的币制初版写成升序 [1,5,10,...,200],而贪心要求降序,会先拿 1 元导致结果荒谬 —— 改为降序 [200,100,...,1]",
    "初版给 TraceStr/TraceCount 加了接受 int 的「占位重载」,与真正的 List<int> 重载造成歧义且无用途 —— 已删除",
    "「贪心在随机币制下有 2.5% 全对」这个结果远低于写作预期(原以为会是 30%~60%),如实保留并写成第二节的开场钩子",
    "实验三初版只测金额 300(贪心恰好正确),容易让读者误以为贪心在 {1,3,4} 上没问题 —— 改为同时列出金额 6(错)和 300(对),并点明「贪心是有时错,不是总是错」"
  ],
  "next": "14.2 区间调度与 Huffman 编码"
}

results matching ""

    No results matching ""