Zata 扎根技术 · 记录思考 · 见更大的世界

Better CodeA Brighter Tomorrow

保持好奇持续成长

Featured image of post 贪心算法:原理、证明与复杂度

贪心算法:原理、证明与复杂度

把「每步选当前最好」还原成一个可判定的形式系统:用五元组刻画贪心,用拟阵给出最优性的充要条件,用交换论证处理非拟阵结构,再用近似比与竞争比处理贪心根本不可能正确的情形。附一份 50 页的完整教程 PDF。

关于贪心算法的中文材料,绝大多数长成同一个样子:先给一句直觉——每步选当前最好的;再列一串经典例子(活动安排、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 张表,含定理索引与符号表。
贪心算法:原理、证明与复杂度
A4 · 50 页 · 3.7 MB · 中文
下载 PDF

目录结构

章内容
第 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 的质量、中文字体与标点的正确处理、以及可脚本化的批量修订。三者叠加之后,这条路反而最短。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计