复习一下贪婪算法。
个人的记忆和理解是:每一步都选择局部最优解。贪婪算法只是一种求得“近似”最优解的算法,并不能保证最终结果是最优解。
这里模仿《算法图解》中的广播覆盖问题模仿提出一个问题。(上述链接提取码为q0m8)
题:现有9个集合,每个集合中各有几个10以内的自然数,现要求尽量用到少的集合凑齐1-10的所有数。
假如有这么几个集合:
a {1, 10, 4, 7}
b {10, 3, 4, 6}
c {3, 4, 5, 7}
d {8, 4, 5}
e {9, 10, 3, 6}
f {8, 5, 7}
g {8, 10, 3, 6}
h {1, 3, 4}
i {8, 1, 3, 5}
j {1, 2, 3, 6}
要用到最少的集合凑齐1到10这些数字,想要把所有可行组合都算出来是一个相当艰难的事情,当然在当前题目下,还是可以花费时间做到的,但是一旦此题的集合数与需要凑齐的数值范围大了以后,计算量就大到代价不能承受的地步了。
这种情况下,只能一步一步,每一步都尽可能凑齐最多的需要的数字,直到所有数字都凑齐,这样得到的最终结果或许不是最优的,但已经接近最优解,并且所花费代价比得到精确的最优解要小得多。综合上来说,这就是”最优解“,这就是贪婪算法。
选出的集合为a--{1, 10, 4, 7},还剩下{2, 3, 5, 6, 8, 9}未得到
选出的集合为e--{9, 10, 3, 6},还剩下{8, 2, 5}未得到
选出的集合为d--{8, 4, 5},还剩下{2}未得到
选出的集合为j--{1, 2, 3, 6},已全部得到
据此策略,我们需要一个”得到局部最优解“的算法去得到当前这一步的最佳选择。
在所有还未被选择的集合中,能凑到最多数字(没有凑到的数字)的集合,就是当前的最优解。
以此分析上面的集合:
- 因为尚未凑到任何一个数,所有第一步只要凑到最多的数字就行了,于是得到了{1, 10, 4, 7}这个集合,当然其他几个长度为4的集合也是当前的最优解之一
- 第一步过后,还剩下{2, 3, 5, 6, 8, 9}这些数字需要凑,那么需要找到能凑到最多这些数字的集合才行,这一步也有{9, 10, 3, 6}和{8, 10, 3, 6}、{8, 1, 3, 5}、{1, 2, 3, 6}这些选择,因为都可以凑到3个数,但因为顺序关系,选择了{9, 10, 3, 6}
- 这一步还剩下{8, 2, 5}这些数字要凑,这一步有{8, 4, 5}、{8, 5, 7}、{8, 1, 3, 5}可以选择,因为都可以凑到2个数,同上面的原因选择了{8, 4, 5}
- 最后需要凑到{2},这一步只有{1, 2, 3, 6}可以选择
用python写了一个随机生成例子来解释贪婪算法。
其中寻找当前最优解的方法是这样的:
def find_best_candidate_set(dict_nums, nums):
"""
选出当前所有集合中,可得到最多‘可用’数字的一个集合(key)
:param dict_nums: 待筛选的集合字典
:param nums: 用于筛选的数字集合
:return: 筛选出的key
"""
best_coverd = set() # 最佳的数字集合
best_coverd_key = None # 最佳的集合所对应的key
for k, v in dict_nums.items():
temp_coverd = nums & v # 当前此集合能得到的数字集合
if len(temp_coverd) > len(best_coverd):
best_coverd = temp_coverd
best_coverd_key = k
return best_coverd_key
在python中,两个set交集可以直接使用“&”符号获得,差集也可使用“-”符号获得。类似于Java中的listA.retains(listB)/listA.removeAll(listB)之类的操作。
全部代码见greedy.py