贪心算法每次都取当前这一步里收益最大的选择,并且之后不再反悔。我用活动安排和分数背包两个能保证最优的例子,把选择规则和 Python 实现写出来。
贪心算法把问题拆成一串选择。每一步只看当前信息,取这一步里收益最大的那个,选完就固定,后面的步骤在剩下的问题里继续。它能得到全局最优,需要两件事同时成立:当前这一步的最优选择可以安全地留在最终答案里,而且剩下的子问题仍然是同一类问题。
活动安排和分数背包满足这两点,所以下面的写法就是最优解。硬币凑整只在币值成套(例如 1、5、10、25)时,每次先拿最大面额才是最优;币值不成套时,这个规则会选错。
活动安排
有若干活动,每个活动占一段连续时间,同一时刻只能进行一个。目标是安排尽可能多的场次。
正确的贪心是:每次选结束最早、并且不和已选活动重叠的那场。结束越早,留给后面的空档越长。按结束时间排序后扫一遍即可。
def select_activities(activities):
chosen = \[\]
end = float("-inf")
for start, finish, name in sorted(activities, key=lambda item: item1):
if start >= end:
chosen.append(name)
end = finish
return chosen
activities = [
(1, 4, "A"),
(3, 5, "B"),
(0, 6, "C"),
(5, 7, "D"),
(3, 9, "E"),
(6, 10, "F"),
(8, 11, "G"),
(8, 12, "H"),
(2, 14, "I"),
(12, 16, "J"),
]
print(select_activities(activities))
输出是 ['A', 'D', 'G', 'J'],共 4 场。时间线是 1--4、5--7、8--11、12--16。C 虽然开始得早,但一直占到 6,会挤掉 A 和 D,所以按结束时间它不会被优先选中。
排序是 O(n log n),随后一次扫描是 O(n)。
分数背包
背包容量有限,每件物品可以只拿一部分,价值按拿走的比例计算。目标是总价值最大。
贪心规则是:优先拿单位重量价值最高的物品,当前这件还能装下多少就拿多少。因为可以拆分,把容量让给单价更高的物品始终更划算。
def fractional_knapsack(items, capacity):
taken = \[\]
total = 0.0
remain = capacity
ranked = sorted(items, key=lambda item: item0 / item1, reverse=True)
for value, weight, name in ranked:
if remain <= 0:
break
portion = min(1.0, remain / weight)
taken.append((name, round(portion, 2), round(value * portion, 2)))
total += value * portion
remain -= weight * portion
return taken, round(total, 2)
items = (60, 10, "a"), (100, 20, "b"), (120, 30, "c")
print(fractional_knapsack(items, 50))
三件物品的单价分别是 6、5、4。容量 50 时先整件拿走 a、b,剩下 20 的容量再拿走 c 的 2/3。结果是:
(('a', 1.0, 60.0), ('b', 1.0, 100.0), ('c', 0.67, 80.0), 240.0)
总价值 240。物品必须整件拿走时,这个「按单价装填」的规则不再保证最优,那种 0-1 背包要用动态规划。
怎么判断能不能用贪心
写之前先回答两个问题:
- 把当前这一步的选择固定下来以后,剩下的最优解接上去,整体仍然最优吗?
- 每一步的比较标准是否只依赖当前状态,不需要回头修改?
两个答案都成立,就按这个标准排序或扫描。活动安排的标准是结束时间,分数背包的标准是单位价值。证明不了第 1 点时,这一步的局部最优可能把后面的更好方案堵死,这时改用动态规划或搜索。