这是一种非常直观且强大的算法设计思想,它在很多问题上都能提供高效且近乎最优的解决方案。
1. 核心思想
贪心算法的核心可以概括为:在每一步选择中都采取当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。
它就像一个“目光短浅”的决策者,只考虑眼前的利益,并坚信通过这一系列局部最优的选择,最终能够达到全局最优。
2. 基本要素
一个问题能否用贪心算法解决,取决于它是否具备以下两个性质:
贪心选择性质
含义:一个问题的全局最优解可以通过一系列局部最优的选择(贪心选择)来达到。
简单说:我们可以通过做出当前最好的选择来构建全局最优解,而不必考虑子问题的解。这是贪心算法与动态规划的主要区别(动态规划需要记录和查询子问题的解)。
最优子结构
含义:一个问题的最优解包含其子问题的最优解。
简单说:解决了大问题的最优解所包含的小问题,自然也是小问题的最优解。这个性质是贪心算法和动态规划都具备的。
3. 基本步骤
设计和实现一个贪心算法通常遵循以下步骤:
问题建模:将问题描述为一组选择,我们需要一步步做出这些选择。
确定贪心策略:明确什么是“当前情况下最优的选择”。这是算法的核心和难点。
证明贪心策略的有效性(关键且困难):必须证明该策略确实能导致全局最优解。通常使用数学归纳法或反证法来证明。
实现算法:根据策略编写代码,通常比较简单,可能只需要一个循环或排序。
4. 经典示例
示例1:找零钱问题(硬币问题)
问题:假设硬币体系为 [1, 5, 10, 20, 50, 100]
元,如何用最少数量的硬币凑出某个金额 X
(比如 123 元)?
贪心策略:每次选择面值最大且不超过剩余金额的硬币。
步骤(凑 123 元):
选 100元,剩余 23元。
选 20元,剩余 3元。
选 1元,重复3次。
总共需要 1+1+3=5 枚硬币。
有效性证明:在这个特定的硬币体系下(每个大面值都是小面值的倍数,称为“规范币制”),贪心策略确实能得到最优解。但注意:如果硬币体系改变,贪心可能失效。例如,如果硬币为 [1, 3, 4]
元,要凑 6元。
贪心:选4 -> 剩2 -> 选1 -> 选1(共3枚:4,1,1)
最优:选3 -> 选3(共2枚:3,3)
此时贪心策略就不是最优的了。
示例2:活动选择问题
问题:有一系列活动,每个活动有开始时间 (Si)
和结束时间 (Fi)
。同一时间只能进行一个活动。如何安排能参加最多数量的活动?
贪心策略:每次选择结束时间最早的活动。这样可以为后续活动留下更多的时间。
步骤:
将所有活动按结束时间
Fi
从小到大排序。选择第一个结束的活动。
接下来,依次选择与已选活动不冲突,且结束时间最早的活动。
这个策略被证明一定能得到全局最优解。
示例3:霍夫曼编码(数据压缩)
问题:给出一组字符及其出现频率,如何用二进制编码每个字符,使得编码后的总长度最短?
贪心策略:自底向上地构建二叉树。每次选择频率最低的两个节点,合并成一个新的节点(新节点的频率为两者之和),重复这个过程直到只剩一棵树。
这个策略被证明能产生最优的前缀编码。
示例4:最小生成树(Prim / Kruskal算法)
问题:在一个带权的连通无向图中,找到一棵边权值之和最小的生成树。
Prim算法的贪心策略:每次选择连接当前树和树外节点的最小权值边。
Kruskal算法的贪心策略:每次选择全图中最小权值边,并且保证加入后不会形成环。
这两种贪心策略都被证明能得到全局最优解。
5. 优缺点
优点:
高效:时间复杂度通常较低。很多贪心算法实质上是线性或近似线性的复杂度(例如,经过一次排序后线性遍历)。
直观:算法通常简单易懂,容易实现。
缺点:
并非万能:很多问题无法使用贪心算法,因为局部最优的累积无法保证全局最优(如上面的
[1, 3, 4]
硬币例子)。证明困难:如何选择和证明贪心策略是正确的是最困难的一步。很多时候需要深厚的数学功底。
6. 如何证明贪心策略的正确性
这是贪心算法的核心难点。常用方法有:
反证法:假设贪心策略得到的解不是最优的,然后推导出矛盾。
数学归纳法:证明通过贪心选择,每一步都能保持最优子结构。
交换论证:假设存在一个最优解
O
,然后通过一步步地将O
中的元素与贪心解G
中的元素进行交换,并且证明交换不会使解变差,最终证明G
至少和O
一样好。
总结
贪心算法是一种“走一步看一步”的算法,它在每一步都做出一个局部最优的决策。它的高效性建立在问题本身具有“贪心选择性质”和“最优子结构”的基础上。
特性 |
描述 |
---|---|
核心思想 |
每一步都采取当前状态下的最优选择,希望最终达到全局最优。 |
关键性质 |
贪心选择性质、最优子结构。 |
优点 |
高效、直观、实现简单。 |
缺点 |
不是对所有问题都有效,策略证明困难。 |
适用场景 |
最短路径、最小生成树、数据压缩、调度问题等。 |
当你遇到一个新问题时,可以先尝试设计一个直观的贪心策略,然后务必思考并证明(或举反例) 它是否能得到全局最优解。如果不能,就需要考虑动态规划、回溯等其他算法范式。