本文属于《算法图解》系列。
一 教室调度问题
假设有如下课程表,你希望将尽可能多的课程安排在某间教室上。
(1) 选出结束最早的课,它就是要在这间教室上的第一堂课。
(2) 接下来,必须选择第一堂课结束后才开始的课。同样,你选择结束最早的课,这将是要在这间教室上的第二堂课。
贪婪算法很简单:每步都采取最优的做法。在这个示例中,你每次都选择结束最早的 课。用专业术语说,就是你每步都选择局部最优解,最终得到的就是全局最优解。
二 背包问题
背包问题:有一个背包,容量为35磅 , 现有如下物品
物品 | 重量 | 价格 |
---|---|---|
吉他 | 15 | 1500 |
音响 | 30 | 3000 |
笔记本电脑 | 20 | 2000 |
要求达到的目标为装入的背包的总价值最大,并且重量不超出。
音响最贵,但背包没有空间装其他东西了。
如果选择装笔记本电脑和吉他,总价将为3500美元!
在这里,贪婪策略显然不能获得最优解,但非常接近。
从这个示例你得到了如下启示:在有些情况下,完美是优秀的敌人。有时候,你只需找到一个能够大致解决问题的算法,此时贪婪算法正好可派上用场,因为它们实现起来很容易,得到的结果又与正确结果相当接近。
三 集合覆盖问题
假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。现有广播台名单如下。
每个广播台都覆盖特定的区域,不同广播台的覆盖区域可能重叠。
如何找出覆盖全美50个州的最小广播台集合呢?听起来很容易,但其实非常难。具体方法如下。
(1) 列出每个可能的广播台集合,这被称为幂集(power set)。可能的子集有2ⁿ 个。
(2)在这些集合中,选出覆盖全美50个州的最小集合。
问题是计算每个可能的广播台子集需要很长时间。由于可能的集合有2ⁿ 个,因此运行时间为 O(2ⁿ )。如果广播台不多,只有5~10个,这是可行的。但如果广播台很多,结果将如何呢?随着广播台的增多,需要的时间将激增。假设你每秒可计算10个子集,所需的时间将如下。
广播台数量n | 子集总数2ⁿ | 需要的时间 |
---|---|---|
5 | 32 | 3.2秒 |
10 | 1024 | 102.4秒 |
32 | 4294967296 | 13.6年 |
100 | 1.26*100³º | 4x10²³年 |
目前并没有算法可以快速计算得到准备的值.
而使用贪婪算法,则可以得到非常接近的解,并且效率高:.
选择策略上,因为需要覆盖全部地区的最小集合:
(1) 选出一个广播台,即它覆盖了最多未覆盖的地区,即便包含一些已覆盖的地区也没关系.
(2) 重复第一步直到覆盖了全部的地区.
这是一种近似算法(approximation algorithm)。在获得精确解需要的时间太长时,可使用近似算法。判断近似算法优劣的标准如下:
这是一种近似算法(approximation algorithm,贪婪算法的一种)。在获取到精确的最优解需要的时间太长时,便可以使用近似算法,判断近似算法的优劣标准如下:
- 速度有多快
- 得到的近似解与最优解的接近程度
在本例中贪婪算法是个不错的选择,不仅运行速度快,本例运行时间O(n²),最坏的情况,假设n个广播台,每个广播台就覆盖1个地区,n个地区,总计需要查询n*n=O(n²),下面看下实现过程。
1 准备数据。
首先,创建一个列表,其中包含要覆盖的州。
最后,需要使用一个集合来存储最终选择的广播台。
final_stations = set()
2 计算答案
你需要遍历所有的广播台,从中选择覆盖了最多的未覆盖州的广播台。我将这个广播台存储在 best_station中。
states_covered是一个集合,包含该广播台覆盖的所有未覆盖的州。for循环迭代每个广 播台,并确定它是否是最佳的广播台。
这里用到了计算求集合的交集。以及判断你检查该广播台覆盖的州是否比 best_station多。
下面来比较一下贪婪算法和精确算法的运行时间。
看下Java版本的实现。
/**
*
* @author bohu83
*
*/
public class GreedyTest {
// 广播站与地区对应关系
static HashMap<String, HashSet<String>> broadcasts = new HashMap<String, HashSet<String>>();
// 需要覆盖的全部地区
static HashSet<String> statesNeed =
new HashSet(Arrays.asList(new String[] { "ID", "NV", "UT", "WA", "MT", "OR", "CA", "AZ" }));
// 所选的广播站
static List<String> finalStaion = new ArrayList<String>();
static {
broadcasts.put("Kone", new HashSet(Arrays.asList(new String[] { "ID", "NV", "UT" })));
broadcasts.put("Ktwo", new HashSet(Arrays.asList(new String[] { "WA", "ID", "MT" })));
broadcasts.put("Kthree", new HashSet(Arrays.asList(new String[] { "OR", "NV", "CA" })));
broadcasts.put("Kfour", new HashSet(Arrays.asList(new String[] { "NV", "UT" })));
broadcasts.put("Kfive", new HashSet(Arrays.asList(new String[] { "CA", "AZ" })));
}
public static void Greedy() {
while (statesNeed.size() > 0) {
String bestStation = null;//将覆盖了最多的未覆盖州的广播台存储进去
//一个集合,包含该广播台覆盖的所有未覆盖的州(有点拗口,可以结合下面的求交集来理解)
HashSet<String> statesCovered = new HashSet<String>();
//循环迭代每个广播台并确定它是否是最佳的广播台
for (String key : broadcasts.keySet()) {
HashSet<String> areas = broadcasts.get(key);
// 求交集
areas.retainAll(statesNeed);
//检查该广播台的州是否比best_station多
if (areas.size() > 0 && areas.size() > statesCovered.size()) {
bestStation = key;
statesCovered = areas;
}
}
// 更新statesneeded
statesNeed.removeAll(statesCovered);
// 把best_station添加到最终的广播台列表中
finalStaion.add(bestStation);
}
System.out.print("finalStaion:" + finalStaion);
}
public static void main(String[] args) {
Greedy();
}
}
输出结果:finalStaion:[Ktwo, Kthree, Kfour, Kfive]
四 NP完全问题
旅行商问题
作者推导一番,假设从两个城市开始,可能的路线有两条。 a->b . b->a
3个城市,从每个城市出发时,都有两条不同的路线,因此总共有6条路线。
假设有4个城市,你选择一个出发城市后,还余下3个城市。而你知道,涉及3个城市时,可能的路线有6条。可能的出发城市有4个,从每个城市出发时都有6条可能的路线,因此可能的路线有4 × 6 = 24条。
这被称为阶乘函数(factorial function),涉及10个城市时,需要计算的可能路线超过300万条。正如你看到的,可能的路线数增加得非常快!因此,如果涉及的城市很多,根本就无法找出 旅行商问题的正确解。
旅行商问题和集合覆盖问题有一些共同之处:你需要计算所有的解,并从中选出最小/最短 的那个。这两个问题都属于NP完全问题。
而使用贪婪算法,随机选择从一个城市出发,比如A,每次选择从最近的还没去过的城市出发,则可以得到近似最优解。
第一次比较n-1个城市
第二次比较n-2个城市
...
第n-1次比较1个城市
第n次不存在需要比较的了个
0+1+2+3+..+(n-1) ≈ O(n²/2)
模拟计算时间对比(每秒计算10次)
数量n | 总数n! | 穷举需要时间 | 贪婪需要时间 |
---|---|---|---|
5 | 120 | 120秒 | 12.5秒 |
10 | 32 | 42天 | 50秒 |
作者还举例挑选橄榄球队员组成球队的问题。
如何判断是NP完全问题的:
1.元素较少时,一般运行速度很快,但随着元素数量增多,速度会变得非常慢
2.涉及到需要计算比较"所有的组合"情况的通常是NP完全问题
3.无法分割成小问题,必须考虑各种可能的情况。这可能是NP完全问题
4.如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题
5.如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题
6.如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题
小结:
贪婪算法寻找局部最优解,企图以这种方式获得全局最优解
对于NP完全问题,还没有找到快速解决方案。
面临NP完全问题时,最佳的做法是使用近似算法。
贪婪算法易于实现、运行速度快,是不错的近似算法。