Greedy 的问题意识
Greedy 算法的形式很简单:每一步都做当前局部最好的选择。
难点不在算法怎么写,而在这个局部选择是否值得相信。
很多问题看起来都可以 greedy:
- 每次选最短的活动
- 每次选价值最大的物品
- 每次选当前最短的边
- 每次选最近的节点
但只有一部分是对的。
Greedy 成功的核心条件是:当前这个选择不会破坏某个全局最优解。换句话说,总存在一个最优解包含 greedy 的第一步选择。
这就是 greedy-choice property。
Exchange argument
Greedy 最常用的证明是 exchange argument。
标准思路:
- 设 $G$ 是 greedy 解,$O$ 是某个最优解
- 找到 $G$ 和 $O$ 第一个不同选择
- 把 $O$ 中对应部分换成 greedy 的选择
- 证明换完后仍然可行,且目标值不变差
- 重复交换,最后得到一个和 greedy 解一致的最优解
这类证明的核心在于解释交换后为什么不变差,形式本身不重要。
如果你找不到交换理由,通常说明 greedy 很可能不成立。
Activity selection
无权活动选择问题:
给一组活动,每个活动有开始时间和结束时间,选择最多数量的互不重叠活动。
正确的 greedy 策略是:
每次选择结束时间最早的活动
为什么不是开始最早?因为开始早不代表给后面留出更多空间。
为什么不是持续时间最短?短活动可能卡在中间,阻断两边更好的组合。
结束时间最早的活动有一个特殊性质:它给剩余活动留下最大空间。
证明:
设 $g$ 是结束时间最早的活动。取任意一个最优解 $O$,设它的第一个活动是 $o$。
因为 $g$ 结束不晚于 $o$,所以把 $O$ 中的第一个活动 $o$ 换成 $g$ 后,后面的活动仍然不冲突。
新解大小不变,所以仍然最优,并且包含 greedy 的第一步选择。
接下来对子问题重复同样论证。
有权活动调度为什么不能 greedy
如果每个活动有 weight,目标变成最大化总价值,结束时间最早就不一定对了。
一个长活动可能价值很高,胜过多个短活动。
这时问题变成 weighted interval scheduling,通常用 DP:
$$ T[k] = \max(T[k-1], v_k + T[p(k)]) $$这个对比很重要。
同样是活动选择:
- 无权:目标是数量,结束最早有 exchange argument
- 有权:目标是价值,局部结束早不保证价值最大,需要 DP 记历史
算法范式取决于 objective,不只取决于题面长得像不像。
MST:Cut Property
Minimum Spanning Tree 是 greedy 的经典成功案例。
MST 的核心正确性来自 cut property:
对任意 cut,跨过这个 cut 的最小权重边是 safe edge,可以被某棵 MST 包含。
直觉是:
如果某棵 MST 没有这条最小 crossing edge $e$,那么加上 $e$ 会形成一个环。这个环里一定还有另一条跨过同一个 cut 的边 $f$。因为 $e$ 是最小 crossing edge,所以:
$$ w(e) \leq w(f) $$用 $e$ 替换 $f$,树仍然连通,权重不增加。
所以选择 $e$ 是安全的。
Kruskal 和 Prim 都是在不断选择 safe edge。
Kruskal 和 Prim 的区别
Kruskal:
- 全局按边权从小到大看边
- 只要加入后不形成环,就加入
- 更像“把很多小树逐渐合并”
- 常用 union-find 判断是否成环
Prim:
- 从任意起点开始维护一棵树
- 每次选择连接当前树和外部节点的最小边
- 更像“从一个点向外扩张”
- 常用 priority queue 找最小 crossing edge
两者的正确性都依赖 cut property。
一些有用结论:
- 环上 maximum weight edge 一定不在某棵 MST 中
- 跨 cut 的 minimum weight edge 是 safe 的
- bridge 一定在 MST 中
- 如果所有边权互不相同,MST 唯一
Dijkstra:Greedy 依赖非负边权
Dijkstra 也是 greedy。
它每次从未确定节点中取出当前距离最小的节点,并认为这个距离已经是最终最短距离。
这个判断成立的原因是:所有边权非负。
如果当前最小距离是 $d[u]$,任何绕到别的未确定节点再回来更新 $u$ 的路径,都只会更长,因为边权不能减少距离。
所以 $u$ 被取出时,$d[u]$ 已经确定。
这就是 Dijkstra 的 invariant。
如果有负权边,这个 invariant 失效。一个节点现在看起来最近,之后可能通过一条负边被改得更短。
所以:
- 非负边权 + shortest path:Dijkstra
- 有负边权但无负环:Bellman-Ford
- DAG:拓扑序 DP 可以处理负边权
算法选择要看约束。Dijkstra 的重点在于非负边权支撑了 greedy 选择,priority queue 只是实现工具。
Bellman-Ford 更像 DP
Bellman-Ford 不做 Dijkstra 那种“当前最小已经最终”的 greedy 判断。
它的思想是:
第 $i$ 轮后,所有最多使用 $i$ 条边的最短路都被正确计算。
因为一条简单最短路最多有 $n-1$ 条边,所以做 $n-1$ 轮 relaxation 就够了。
这更像 DP:
$$ dist_i[v] = \min_{(u,v)\in E}(dist_{i-1}[u]+w(u,v)) $$实现上可以用一个数组反复松弛,但证明思路是按路径边数做 induction。
这也解释了为什么 Bellman-Ford 可以处理负边:它没有提前确认某个点的最终距离。
Huffman coding
Huffman coding 的目标是给字符设计 prefix-free code,使得期望编码长度最小。
Greedy 策略:
每次合并频率最小的两个节点
直觉是:频率越低的字符越应该放在更深的位置。最深的两个叶子应该是频率最低的两个字符,而且它们可以做 sibling。
所以每次把两个最低频率字符合并成一个新节点,问题规模减少 1。
这个证明也有 exchange flavor:如果最深 sibling 不是两个最低频率字符,可以交换标签,不会增加总代价。
Fractional knapsack 和 0/1 knapsack
Fractional knapsack 可以 greedy。
每个物品可以切分时,按单位价值排序,从高到低装入背包,就是最优。
因为任意一个低单位价值的重量,都可以被更高单位价值的重量替换,价值不会变差。
0/1 knapsack 不能这样做。
物品不可切分,局部单位价值最高的物品可能占用容量,导致错过更好的组合。
所以:
- Fractional knapsack:greedy
- 0/1 knapsack:DP / NP-hard decision variant
同一个题面,是否允许切分,会改变整个算法范式。
Greedy 失败的常见原因
Greedy 失败通常有几种模式:
- 局部选择占用了关键资源,后续无法补救
- 当前收益最大,不代表组合收益最大
- 当前结束最早、距离最近、重量最小,只是某个维度好,不代表目标函数好
- 问题有隐藏的全局约束,需要状态记录
遇到一个新问题,如果想用 greedy,先问:
- 这个选择是否可以被交换进某个最优解
- 选择后剩余问题是否和原问题同型
- 是否存在一个小反例打破局部选择
如果这三个问题答不清楚,greedy 就不能直接用。
Greedy 和 DP 的分界
Greedy 和 DP 都依赖 optimal substructure。
区别在于:
- Greedy 只需要保留一个当前选择
- DP 要保留多个状态,因为不同历史会影响未来
Activity selection 无权版本里,选最早结束活动后,剩余问题只看它之后的活动,历史不重要。
Weighted interval scheduling 里,活动价值改变了选择的比较方式,需要保留前缀最优值。
Dijkstra 里,非负边权保证当前最短的点不会被未来路径反超。
Bellman-Ford 里没有这个保证,只能按路径长度逐轮更新。
Part III 的核心判断就是:局部选择有没有足够强的结构保证。能证明,就 greedy;不能证明,通常要 DP、graph search、flow 或更复杂的模型。