1-算法 - 贪心算法
about
贪心算法,又称贪婪算法,指的是,在对问题求解时,总是做出在当前看来是最好的选择。
也就是说,贪心算法不从整体最优进行考虑,而是只考虑当前(局部)的最优解。
贪心算法并不保证会得到最优解,但是在某些问题上,贪心算法的解就是最优解,这也就意味着,遇到相关问题时,我们要会判断解决当前问题,能否使用贪心算法来解决问题。
找零问题
假设商店老板需要找零n元钱,钱币的面额有:100、50、20、5、1元,且各个面额钱数量很多,总之能找开!
那么,问题来了:如何找零使得所需钱币的数量最少?
实现:
def greed(t, n):
"""
贪心算法实现找零问题
:param t: 不同面额的金钱列表,这个列表需要降序排序,要从最大面额的开始找
:param n: n元钱,表示要对n元钱找零
:return: 找零结果,根据传过来的值可以预期找零应该是 3张10的;1张50的,1张20的,1张5块的;一张1块的。
"""
t.sort(reverse=True) # 降序排序
m = [0 for _ in t]
for i, money in enumerate(t):
"""
思路是:
第一次循环,先用100的找
m[i] = n // money 表示 376整除100等于3,表示100的需要找三张
n %= money 找完100的,还剩多少呢?还剩76
第二次循环:
用76整除50等于1,也就是50的需要1张,零头还剩26块
第三次循环:
用26整除20等于1,表示20的也需要1张,零头还剩6块
第三次循环:
用6整除5等于1,表示5块的需要1张,零头还剩1
第四次循环:
1整除1等于1,表示1块需要1张,此时n就等于0,表示找开了,循环也结束了
"""
m[i] = n // money
n %= money
# print(m, n, money)
return m, n
t = [100, 50, 20, 5, 1]
print(greed(t, 376)) # ([3, 1, 1, 1, 1], 0) # 返回的m表示找零结果是3张10的;1张50的,1张20的,1张5块的;一张1块的,而 n 值0表示找开了
# print(greed(t, 376.5)) # ([3.0, 1.0, 1.0, 1.0, 1.0], 0.5),0.5表示省5毛钱找不开,因为我们的t中只定义到了元,没有定义角
找零问题中,贪心就贪心在,先尽可能的给最大面额的,然后次之,这样最后找零的金钱数量是最少的。
背包问题
一个小偷在某个商店发现有n
个商品,第i
个商品价值vi,重wi千克,他希望拿走的价值尽量高,但他的背包最多只能容纳W
千克的东西。
问题来了,他应该如何分配,才能让拿走的商品价值最高?
背包问题还可以进一步细分:
0-1
背包:有n种商品,且每种商品只有1个。这也就意味着,对于一个商品,小偷要么把它完整拿走,要么留下,不能只拿走部分,或者把一个商品拿走多次,比如商品为金条。- 分数背包:对于一个商品,小偷可以拿走其中任意一部分,比如商品为金砂。
思考:对于上面两种背包来说,贪心算法是否都能得到最优解?为什么?
首先来分析分数背包。
现有(价值v;重量千克):
- 金砂:v1=120;w1=10kg
- 银砂:v1=100;w1=20kg
- 铜砂:v1=80;w1=30kg
- 背包容量:W=50kg
根据常识,我们优先拿走所有的金砂,其次是所有银砂,如果背包还有空的话,再把铜砂也拿走。所以,结果就是金砂拿走10kg;银砂拿走20kg;此时背包容量还剩20kg,所以拿走铜砂20kg,背包满了,走人....
再来分析0-1
背包。
现有(价值v;重量千克),如一根金条重10kg,价值120:
- 一根金条:v1=120;w1=10kg
- 一根银条:v1=100;w1=20kg
- 一根铜条:v1=80;w1=30kg
- 背包容量:W=50kg
根据常识,我们还是优先拿金条,然后是银条,此时背包容量还剩20kg,铜条这里就装不下了,此时的价值是120+100=220
。
这背包不满啊,我们再尝试其他的组合,拿银条和铜条,二者加一块正好是50kg,那价值几何?100+80=180
。
还剩最后的组合,拿金条和铜条,其价值120+80=200
。
这么一算,贪心算法就不一定适用了,因为该算法得到的结果不一定是最优解(背包不一定是满的)。
综合考虑,贪心算法只能解决分数背包的问题。来看下代码示例吧。
示例:
# 金砂 银砂 铜砂
goods = [(120, 10), (100, 20), (80, 30)] # 每个商品元组(价值,重量)
goods.sort(key=lambda x: x[0] / x[1], reverse=True) # 根据单价降序排序,因为要从价值最高的开始拿,尽可能的多拿,所以要将最有价值的排在首位
def fractional_backpack(goods, w):
"""
分数背包实现
:param goods: 商品列表
:param w: 背包重量
:return:
m: 存储每种商品拿多少,它是介于0~1之间数字,1表示拿走当前商品的所有,零点几表示拿走一部分
"""
m = [0 for _ in range(goods.__len__())]
total_v = 0 # 拿走商品的总价值
for i, (price, weight) in enumerate(goods):
if w >= weight: # 表示背包容量大于当前商品的重量,可以将这个商品都拿走
m[i] = 1 # 拿走所有商品
w -= weight # 背包容量要减去拿走商品的重量
total_v += price # 拿走当前商品的总价值
else: # 表示当前背包容量不足以拿走当前完整的商品,只能拿走一部分,这一部分是多少,是需要计算的
m[i] = round(w / weight, 2) # 当前背包的容量除以当前商品的重量,也就是拿走一部分
w = 0 # 拿走的这一部分商品,正好填满背包的容量,也就是背包满了
total_v += m[i] * price # 部分商品的价值
break # 背包满了,推出就行了
return total_v, m
print(fractional_backpack(goods, 50)) # (273.6, [1, 1, 0.67])
分数背包能使拿走的商品价值最大化。
数字拼接问题
有n个非负整数,将其按照字符串拼接的方式拼接为一个整数,如何拼接可以使拼接出的整数最大?
如32, 94, 128, 1286, 6, 71,这几个值比较判断......最终得到的结果是94716321286128。
问题:字符串类型的数字如何比较大小?
比较两个字符串类型的整数大小,利用的是其在ASCII编码的位置来决定大小的。
字符串的编码比较机制就是从字符串第一位开始比较,如果相等,再比较第二位,以此类推,得出较大的字符串。
对于长度不同的字符串,前面的都相等,后面的有字符的大:
print('6' > '32') # True
print('96' > '87') # True
print('128' > '1276') # True
print('128' > '1286') # False
坑
上面的比较逻辑都没问题,但如果用于作为数字拼接问题的解决算法,就存在问题了。
首先,对于长度相等的没问题。但对于长度不相等的肯定有问题了,如下面的两组数字进行拼接:
a, b = "128" "1286" # b>a拼接结果如下:
b + a = "1286128" # 拼接结果要比a+b大
a + b = "1281286"
# 上面这种情况没问题
# 但下面这种情况就有问题了
c, d = "728" "7286" # d>c拼接结果
d + c = "7286728" # d>c的拼接结果要比c>d大
c + d = "7287286"
对于上面存在的问题,怎么解决呢?也好办,我们将两组字符串进行拼接后比较:
# before
c, d = '728', '7286'
r = c + d if c > d else d + c
print(r) # 7286728
# after
c, d = '728', '7286'
r = c + d if c + d > d + c else d + c
print(r) # 7287286
按照优化后的思路实现代码:
from functools import cmp_to_key
def my_cmp(x, y):
""" 对比两个值返回对比结果 """
if x + y < y + x:
return 1
elif x + y > y + x:
return -1
else: # 相等
return 0
def number_join(li):
"""
数字拼问题
:param li: 带拼接的数字列表
:return: 拼接后的字符串
"""
# 首先,将列表中的整数都转换为字符串,方便后后续比较
li = list(map(str, li))
# 然后对列表进行降序排序,将最大的放到最前面
# 这一步关键就是根据值的大小进行交换,如: ["32", "94"] --> ["94", "32"]
li.sort(key=cmp_to_key(my_cmp))
# 上面一步执行完,得到的降序列表就是待拼接前的列表了
# print(li) # ['94', '71', '6', '32', '1286', '128']
# return ''.join(li) # 94716321286128
return ','.join(li) # 为了便于观察,这里以逗号分进行分割
li = [32, 94, 128, 1286, 6, 71]
print(number_join(li)) # 94,71,6,32,1286,128
上面使用的是内置排序算法,你也可以使用其他的排序算法来完成排序。
参考:
活动选择问题
假设有n个活动要举行,这些活动要占用同一片场地,但场地在同一时刻只能举办一个活动。
每个活动都有一个开始时间si和结束时间fi(这里的时间以整数表示)。
问题:如下图,有11场活动,如何安排才能使这个场地能够举办的活动最多?
贪心结论:最先结束的活动一定是最优解的一部分。
证明:假设活动a是所有活动中最先结束的活动,活动b是最优解中最先结束的活动。
- 如果
a=b
,结论成立。 - 如果
a!=b
,则活动b的结束时间一定晚于活动a的结束时间,此时用活动a替换掉最优解中的活动b,活动a一定不与最优解中的其他活动时间重叠,因此替换后的活动a也是最优解。
代码示例:
activities = [
(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)
] # 元组(活动开始时间,活动结束时间)
def activity_selection(a):
"""
活动选择问题
:param a: 所有活动列表
:return m: 选择出的活动列表
"""
# 首先根据最先结束的活动进行降序排序活动列表
a.sort(key=lambda x: x[1])
m = [a[0]] # 将最先结束的活动先放到m中
for i in range(1, len(a)): # 最先结束的活动放到了m中,这里直接从第二个开始循环,挨着看每个活动跟m中的最后一个活动是否冲突
# 在循环中判断,当前活动和m中的最后一个活动进行时间对比
# 如果当前活动的开始时间小于m中的最后一个活动的结束时间,表示冲突了,这个当前活动就被抛弃掉
# 如果当前活动的开始时间大于等于m中的最后一个活动的结束时间,表示不冲突,可以将这个活动添加到m中
# 也允许这种活动:8-10点,10-12点,即允许在结束的时间点,开始下一场活动
if a[i][0] >= m[-1][1]:
m.append(a[i])
return m
print(activity_selection(activities)) # [(1, 4), (5, 7), (8, 11), (12, 16)]