关于贪心算法的中文材料,绝大多数长成同一个样子:先给一句直觉——每步选当前最好的;再列一串经典例子(活动安排、Huffman、最小生成树);最后用一句「可以证明这是正确的」收尾。读完的人记住了案例,但没有获得判断新问题的能力。下次遇到一个没见过的贪心策略,仍然只能靠试。
这份教程换一条路。它把「贪心是否正确」当成一个可判定的命题来处理,而不是一种需要灵感的技巧。下面是我在写这份 50 页教程时形成的几个核心结论,以及它们为什么值得单独成篇。
一、贪心不是算法,是算法模板
大多数人把贪心当成一类算法,但它其实是一族共享同一骨架的算法。把骨架写出来之后会发现,所谓不同的贪心策略,差别只落在五元组里的两三个位置上:
\[ \mathcal{G} = (E,\ \sigma,\ \kappa,\ \mathcal{F},\ S_0) \]- \(E\):候选元素的全集;
- \(\sigma: E \to \mathbb{R}\):评价函数,给出优先级(这一步决定了「贪」在哪);
- \(\kappa: 2^E \to \{0,1\}\):可行性判定;
- \(\mathcal{F} = \{S \subseteq E : \kappa(S) = 1\}\):可行域,通常向下封闭;
- \(S_0 = \varnothing\):初始状态。
算法就是反复执行「按 \(\sigma\) 降序扫描,若 \(S \cup \{e\}\) 可行则并入」这一条规则。
这个视角带来的第一个好处是:三个经典问题的差别被压缩到了一个参数上。
| 问题 | 评价函数 \(\sigma\) | 结果 |
|---|---|---|
| 分数背包 | 单位重量价值 \(v_i/w_i\) | 最优 |
| 0-1 背包 | 单位重量价值 \(v_i/w_i\) | 错误 |
| 带截止期的任务调度 | 截止期 \(d_i\) 升序 | 最优 |
三者的代码结构完全一致——排序、遍历、判定、接受。分数背包与 0-1 背包用的是同一个评价函数,一个对、一个错。这说明正确性根本不由评价函数决定,而由可行域 \(\mathcal{F}\) 的代数结构决定。这是全文的出发点。
二、拟阵:唯一一个充要条件
如果问「什么样的可行域能让贪心对任意权重都返回最优」,答案是唯一的,而且可以完整刻画。
定理(Rado–Edmonds) 独立系统 \(M = (E, \mathcal{I})\) 上按权重降序的贪心算法对所有 \(w: E \to \mathbb{R}_{\ge 0}\) 都返回最大权基,当且仅当 \(M\) 是拟阵。
拟阵只需要三条公理:非空、遗传(向下封闭)、交换(\(|A| < |B|\) 时存在 \(e \in B \setminus A\) 使 \(A \cup \{e\}\) 仍独立)。第三条是全部力量的来源——它保证了「局部的贪心选择总能被某个全局最优解容纳」。
这个定理的价值在于它是双向的。正向告诉你:识别出拟阵,正确性就免费了,不需要再写交换论证。最小生成树就是最好的例子——Kruskal 与 Prim 的正确性可以完全由「图拟阵」这三个字推出,割性质那套证明在逻辑上是冗余的(第 4 章仍然给了一遍,因为那套证法对理解结构更有帮助)。
反向更狠:不是拟阵,就必然存在一组权重让贪心失败。这不是「我们的策略没设计好」,而是结构层面的判决。所以遇到贪心失效时,正确的动作不是换评价函数继续试,而是先判断可行域是不是拟阵。
三、三条失效边界
拟阵之外的世界很大,但有三个边界特别值得记住,因为几乎所有「贪心错了」的案例都落在它们附近。
边界一:Huffman 不在其中。 最优前缀码的可行解是二叉树结构,不是 \(E\) 的子集。\(\mathcal{F}\) 在这里连定义都没有,拟阵理论完全失效。它的正确性只能靠交换论证。
边界二:背包不在其中。 容量约束下的 0-1 背包不满足交换公理:取 \(W=10\),\(A=\{w=9\}\),\(B=\{w=5, w=4\}\),则 \(|A| < |B|\) 但 \(A\) 无法再扩展。于是比值贪心确实会失败,且这个失败是结构性的。
边界三:最短路不在其中。 路径不能构成子集族意义上的独立系统——两条路径的并集通常不是路径。Dijkstra 的正确性依赖的是非负边权这一数值条件,而不是任何组合结构。
四、交换论证:覆盖剩下的一切
不在拟阵里的问题,正确性怎么证?答案是一个统一的模板,我把它的证明负担压缩到了一句话:
给定一个已经包含前 \(i-1\) 步选择的最优解,能否改造它使其也包含第 \(i\) 步的选择,且不损失最优性。
只要能对每个 \(i\) 回答「能」,贪心就正确。所有交换论证——Huffman 的最深兄弟、Kruskal 的割交换、Dijkstra 的前缀替换——都是这一句话的不同实现方式。
这个模板还有一个不太被强调的推论:贪心输出的必然是一个极大可行解。因为任何被拒绝的元素在它被考察的那一刻就已经不可加入,而向下封闭性保证了之后更不可能。所以「最优解包含 \(G\)」与「\(G\) 极大」两条一夹,就只剩 \(G\) 本身。
五、失效之后:近似比与竞争比
拟阵覆盖不到的问题里,贪心往往不能给出最优解,但能给出有保证的近似解。这时候讨论的对象从「对不对」变成「差多少」。
集合覆盖的贪心给出 \(H_n \approx \ln n\) 的近似比,且这个界是紧的;带基数约束的次模最大化给出 \(1 - 1/e \approx 0.632\) 的保证因子,Nemhauser 证明了这也是最优的——任何多项式算法都做不到更好。这两个数字值得记住,因为它们划定了贪心在组合优化里的能力上限。
在线场景又是另一种困难:不是算不动,而是不知道未来。竞争比衡量的是「与离线最优的比值」。缓存替换的 LRU 有 \(k\) 竞争比且对所有确定性算法都是最优的,但随机化可以把界压到 \(O(\log k)\)——信息缺失造成的损失,可以用随机性部分买回来。
这两种「最坏情况」形式相同、来源完全不同,第 5、6 章对照阅读会更清楚。
六、还有一件事:实现层面
理论正确不等于代码正确。第 7 章专门处理那些让贪心在工程里悄悄出错的细节:比较器破坏传递性导致的排序未定义行为、用 \(\epsilon\) 比较浮点数破坏全序、平局处理对结果一致性的影响、以及「贪心选择性质」在增量更新场景下的失效。
其中最容易踩的是第一条。\(\epsilon\) 容差比较看起来温和,实际上它连传递性都不满足,而排序算法的正确性前提就是严格弱序——一旦破坏,结果是未定义行为,不是「差不多正确」。
关于这份 PDF
上面这些结论的完整推导、证明与习题,整理成了一份 50 页的教程:
- 8 章 + 附录,52 个小节;
- 42 条定理、引理、定义与推论,全部带证明,共 30 处证明;
- 61 个编号公式,覆盖拟阵的秩函数、Kraft 不等式、近似比上界、竞争比分析;
- 35 道习题,附解答提示;
- 5 幅图、8 张表,含定理索引与符号表。
目录结构
| 章 | 内容 |
|---|---|
| 第 1 章 | 贪心的形式化框架:五元组模型、决策序列、冲突与可回溯性 |
| 第 2 章 | 拟阵:贪心最优性的代数刻画(Rado–Edmonds),三条失效边界 |
| 第 3 章 | 交换论证:通用证明模板,Huffman 的压缩论证 |
| 第 4 章 | 图上的贪心:Kruskal、Prim、Dijkstra 的割性质与正确性 |
| 第 5 章 | 近似:集合覆盖的 \(H_n\) 界、次模的 \(1-1/e\) 界 |
| 第 6 章 | 在线与随机:竞争比、LRU 的 \(k\) 界、随机化的收益 |
| 第 7 章 | 复杂度工程:平局、浮点精度、比较器陷阱 |
| 第 8 章 | 五个完整案例 + 习题解答提示 |
| 附录 | 定理索引、符号表、参考文献、排版说明 |
三条阅读路线
- 想尽快用起来:1 → 3 → 7 → 8。先建判定框架,再学交换论证,最后处理实现陷阱。
- 关心为什么对:2 → 3 → 5。拟阵给代数解释,交换论证覆盖非拟阵,近似比处理失效补偿。
- 做近似与在线:5 → 6 对照读,注意两种「最坏情况」的区别。
一点排版上的说明
这份 PDF 的产生流程本身也是「用合适的工具做合适的事」的例子。内容以 Markdown 撰写;公式由自建的一个 LaTeX 子集转换器编译成 MathML(覆盖分式、根式、求和与积分、矩阵环境、定界符自动伸缩);最后由 Chromium 排版引擎按 A4 输出,页边距、页眉页脚与分页规则由 CSS Paged Media 控制。中文正文用宋体,西文与数学符号用 STIX Two,标题用黑体,行内公式与汉字间距做了紧缩处理。
之所以没用现成的排版链,是因为需要在离线环境下同时满足三个要求:数学公式接近 LaTeX 的质量、中文字体与标点的正确处理、以及可脚本化的批量修订。三者叠加之后,这条路反而最短。