贪心算法
贪心算法(又称贪婪算法)是指,在堆问题求解时,总是做出当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出是在某种意义上的局部最优解。
贪心算法并不保证会得到最优解,但是在某些问题上贪心算法的解就是最优解。要会判断一个问题能否用贪心算法来计算。
一、找零问题
假设商店老板需要找零n元钱,钱币的面额有:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最小?
t = [100, 50, 20, 5, 1] def change(t, n): # n指的总金额 m = [0 for _ in range(len(t))] # 创建一个和t一样长但全是0的列表 # 这里假设t都是倒序排好的 for i, money in enumerate(t): m[i] = n // money # n整除money:376//100=3 n = n % money # n对money取余:376%100=76 return m, n # 如果到最后找不开,n就是找不开的钱 print(change(t, 376)) # ([3, 1, 1, 1, 1], 0) 即:300+50+20+5+1 # 如果t = [100, 50, 20, 5],则有1块钱找不开, print(change(t, 451)) # ([4, 1, 0, 0], 1)
二、背包问题
一个小偷在某个商店发现有n个商品,第i个商品价值vi元,重wi千克。他希望拿走的价值尽量高,但他的背包最多只能容纳W千克的东西。他应该拿走哪些商品?
1、0-1背包
对于一个商品,小偷要么把它完整拿走,要么留下。不能只拿走一部分,或把一个商品拿走多次。(商品为金条)
2、分数背包
对于一个商品,小偷可以拿走其中任意一部分。(商品为金砂)
3、问题示例
- 商品1:v1=60 w1=10
- 商品2:v2=100 w2=20
- 商品3:v3=120 w3=30
- 背包容量:W=50
- 对于0-1背包和分数背包,贪心算法是否都能得到最优解?为什么?
答:贪心算法对分数背包可以得到最优解,对0-1背包得不到最优解,很可能背包都无法装满。
两种背包问题都具有最优子结构性质。对0-1背包问题,考虑重量不超过W而价值最高的装包方案。如果我们将商品j从此方案中删除,则剩余商品必须是重量不超过W-wj的价值最高的方案(小偷只能从不包括商品j的n-1个商品中选择拿走哪些)。
虽然两个问题相似,但我们用贪心策略可以求解背包问题,而不能求解0-1背包问题,为了求解部分数背包问题,我们首先计算每个商品的每磅价值vi/wi。遵循贪心策略,小偷首先尽量多地拿走每磅价值最高的商品,如果该商品已全部拿走而背包未装满,他继续尽量多地拿走每磅价值第二高的商品,依次类推,直到达到重量上限W。因此,通过将商品按每磅价值排序,贪心算法的时间运行时间是O(nlgn)。
为了说明贪心这一贪心策略对0-1背包问题无效,考虑下图所示的问题实例。此例包含3个商品和一个能容纳50磅重量的背包。商品1重10磅,价值60美元。商品2重20磅,价值100美元。商品3重30磅,价值120美元。因此,商品1的每磅价值为6美元,高于商品2的每磅价值5美元和商品3的每磅价值4美元。因此,上述贪心策略会首先拿走商品1。但是,最优解应该是商品2和商品3,而留下商品1。拿走商品1的两种方案都是次优的。
但是,对于分数背包问题,上述贪心策略首先拿走商品1,是可以生成最优解的。拿走商品1的策略对0-1背包问题无效是因为小偷无法装满背包,空闲空间降低了方案的有效每磅价值。在0-1背包问题中,当我们考虑是否将一个商品装入背包时,必须比较包含此商品的子问题的解与不包含它的子问题的解,然后才能做出选择。这会导致大量的重叠子问题——动态规划的标识。
4、分数背包代码实现
def fractional_backpack(goods, w): """ 贪心算法——分数背包 :param goods: 商品 :param w: 背包容量 :return: """ m = [0 for _ in range(len(goods))] # [0, 0, 0] total_v = 0 # 总价值 for i, (price, weight) in enumerate(goods): if w >= weight: m[i] = 1 total_v += price w -= weight else: # 不足1 m[i] = w / weight total_v += m[i]*price w = 0 break return m, total_v print(fractional_backpack(goods, 50)) # ([1, 1, 0.6666666666666666], 240.0) 60+100+120*2/3=60+100+80=240
三、拼接最大数字问题
有n个非负整数,将其按照字符串拼接的方式拼接为一个整体。如何拼接可以使得得到的整数最大?
例:32,94,128,1286,6,71可以拼接出的最大整数为94716321286128
from functools import cmp_to_key # 传递python2中sort的cmp函数 li = [32, 94, 128, 1286, 6, 71] def xy_cmp(x, y): if x+y < y+x: # 要让大的在前面,这里需要交换 return 1 elif x+y > y+x: return -1 else: return 0 def number_join(li): li = list(map(str, li)) # 转换为字符串:['32', '94', '128', '1286', '6', '71'] # 方法1:cmp函数 li.sort(key=cmp_to_key(xy_cmp)) # 传递进xy_cmp函数 # 方法2:使用快排、冒泡排序等 return "".join(li) print(number_join(li)) # 94716321286128
四、活动选择问题
假设有n个活动,这些互动要占用同一片场地,而场地在某时刻只能供一个活动使用。
每个活动都有一个开始时间si和结束时间fi(题目中时间以整数表示),表示活动在[si, fi)(左闭右开)区间占用场地。
问:安排哪些活动能够使该场地举办的活动个数最多?
1、贪心结论和证明
贪心结论:最先结束的活动一定是最优解的一部分。
证明:假设a是所有活动中最先结束的活动,b是最优解中最先结束的活动。
- 如果a=b,结论成立。
- 如果a≠b,则b的结束时间一定晚于a的结束时间,则此时用a替换掉最优解中的b,a一定不与最优解中的其他活动时间重叠,因此替换后的解也是最优解。
2、活动选择问题实现
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)] # 保证活动是按照结束时间排好序的(每次选择最早结束的) activities.sort(key=lambda x: x[1]) # print(activities) def activity_selection(a): """ 活动选择 :param a: 活动 :return: """ res = [a[0]] # 将最早结束的活动放入结果中 for i in range(1, len(a)): # 对列表进行遍历(除去第一个) if a[i][0] >= res[-1][-1]: # 当前活动开始时间 大于等于 res最后一个活动的结束时间 因此不冲突 res.append(a[i]) return res print(activity_selection(activities)) # [(1, 4), (5, 7), (8, 11), (12, 16)]
五、总结
都是最优化问题。大大提升了解决问题的速度,但是它也有一些优化问题无法解决或得到的不是最优解。
贪心算法存在的问题:
- 不能保证求得的最后解是最佳的;
- 不能用来求最大或最小解问题;
- 只能求满足某些约束条件的可行解的范围。