一、贪心算法
1、基础概念
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
贪心算法的要素:
-
贪心选择
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。 -
最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法或动态规划算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响,而动态规划则不是。贪心算法对每个子问题的解决方案都做出选择,不能回退;动态规划则会根据以前的选择结果对当前进行选择,有回退功能。动态规划主要运用于二维或三维问题,而贪心一般是一维问题 。
2、基本思路
思想:
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止 [3] 。
过程:
- 建立数学模型来描述问题;
- 把求解的问题分成若干个子问题;
- 对每一子问题求解,得到子问题的局部最优解;
- 把子问题的解局部最优解合成原来解问题的一个解。
3、算法特性
贪婪算法可解决的问题通常大部分都有如下的特性:
- 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
- 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
- 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
- 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
- 最后,目标函数给出解的值。
- 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。
4、实例
1找零问题
假设商店老板需要找零n元钱,钱币的面额右:100元、50元、20元、5元、1元,如何找零使得所需钱币的数量最少?
# __author__: PPPsych
# date: 2021/1/9
def change(t, n):
"""
:param t:面额
:param n:各面额找零数
:return:
"""
m = [0 for _ in range(len(t))]
for i, money in enumerate(t):
m[i] = n // money
n = n % money
return m, n
if __name__ == '__main__':
print('——————————测试——————————')
t = [100, 50, 20, 5, 1]
print(change(t, 376))
2背包问题
一个小偷在某商店发现有n个商品,第i个商品价值vi元,重wi千克。他希望拿走的价值尽量高,但他的背包最多只能容纳w千克东西。他应该拿走那些商品?
注:
-
0-1背包:对于一个商品,小偷要么全拿走,要么留下,不能之拿走一部分,或把一个商品拿走多次。
-
分数背包:对于一个商品,小偷可拿走其中一部分。
# __author__: PPPsych
# date: 2021/1/9
def fractional_backpack(goods, w):
"""
:param goods: 商品
:param w: 背包容量
:return:
"""
m = [0 for _ in range(len(goods))] # 各商品数量
total = 0 # 总价值
for i, (prize, weight) in enumerate(goods):
if w >= weight:
m[i] = 1
total += prize
w -= weight
else:
m[i] = w / weight
total += m[i] * prize
w = 0
break
return total, m
if __name__ == '__main__':
print('——————————测试——————————')
goods = [(60, 10), (100, 20), (120, 30)] # (价格,重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True)
print(fractional_backpack(goods, 50))
3拼接最大数字问题
有n个非负数,将其按照字符串拼接的方式拼接为一个整数。如何拼接可以使得得到最大整数?
例如:32,94,128,1286,6,71可以拼接的最大整数为94716321286128
# __author__: PPPsych
# date: 2021/1/9
from functools import cmp_to_key
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))
li.sort(key=cmp_to_key(xy_cmp))
return "".join(li)
if __name__ == '__main__':
print('——————————测试——————————')
li = [32, 94, 128, 1286, 6, 71]
print(number_join(li))
4活动选择问题
假设有n个活动,这些活动要占用同一片场地,而场地在某时刻只能提供一个活动使用。每个活动都有一个开始时间si和结束时间fi(题目中时间以正数表示),表示活动在 [si,fi)区间占用场地。问:安排哪些活动能使该场地举办的活动个数最多?
贪心结论:最先结束的活动一定是最优解的一部分
证明:假设a是所有活动中最先结束的活动,b是最优解中最先结束的活动。
- 如果a=b,结论成立
- 如果a!=b,则b的结束时间一定晚于a的结束时间,则此时用a替换掉最优解中的b,a一定不予最优解中的其他活动时间重叠,因此替换后的解也是最优解。
# __author__: PPPsych
# date: 2021/1/9
def activity_selection(a):
res = [a[0]]
for i in range(1, len(a)):
if a[i][0] >= res[-1][1]:
# 当前