【核心算法6】贪心算法
贪心算法就是遵循某种既定原则,不断地选取当前条件下最优的选择来构造每一个子步骤的解决方案,直到获得问题最终的求解。在对问题求解时,总是做出在当前看最好的选择。
也就是说,不从整体最优上考虑,所做的仅是在某种意义上的局部最优解。
利用贪心算法解题,需要解决两个问题
-
问题是否适合用贪心算法求解
所求问题是否具有贪心选择性质 贪心选择性质:是指应用同一种规则F,将原问题变为一个相似但规模更小的子问题,后面的每一步都是当前看来最佳的选择。这种选择依赖于已作出的选择,但不依赖与未作出的选择。从全局看,运用贪心策略解决的问题在程序的运行过程中无回溯过程。
-
问题是否具有局部最优解
问题具有局部最优解,从而选择一个贪心标准,得到问题的最优解 解题思路: 1. 建立对问题精确描述的数学模型,包括定义最优解的模型 2. 将问题分成一系列子问题,同时定义子问题的最优解结构 3. 应用贪心算法可以确定每个子问题局部最优,并根据最优模型,用子问题的局部最优解堆叠出全局最优解
- 硬币找零问题
- 活动安排问题
- 哈夫曼编码
硬币找零问题
问题描述
用最少硬币支付指定额度问题
假设,有6种不同面值的硬币,各硬币的面值分别为5分,1角,2角,5角,1元,2元。现要用这些面值的硬币来购物和找零。购物时规定了可以使用的各种面值的硬币个数。假定商店里面各面值的硬币足够多,顾客也可以用多种方式支付。在一次购物中,希望使用最少硬币个数
举例,一名顾客需要付款0.55元,但顾客没有5角的硬币
-
第一种情况
付款:0.2 + 0.2 + 0.1 + 0.05 = 0.55
找零:0
-
第二种情况
付款:1
找零:0.2 + 0.2 + 0.05
-
第三种情况
付款:1 + 0.05
找零:0.5
那么,问题来了,对于给定的各种面值的硬币个数和付款金额,如何计算使用硬币个数最少的交易方案。
其核心思想为消费者硬币数量有限,商店的硬币数量无限,用公式描述:
- MIN(消费者支付硬币个数 + 商店找零硬币个数)
- 支付值 - 找零值 = 商品值
则问题转换为寻找上面两个问题的最优解,其贪心算法为:
MAX(消费者拥有的硬币面值 - 商店拥有的硬币面值) 优先使用
假如,消费者拥有2元的硬币,商店拥有5分的硬币,因此MAX :2元 - 5分 = 195分
组合所有情况:
2元(不找零),2元 - 5分, 2元 - 1角,...... 5分(不找零)
接着验证该算法的贪心选择性和最优子结构性质,证明贪心算法可以获得最优解。
代码实现
import time
# 每种硬币的面值
coins = [0.05, 0.1, 0.2, 0.5, 1, 2]
# 每种硬币的数量
coin_num = []
s = 0
print("Separated by ','")
temp = input('Please enter the quantities of various change:')
coin_num0 = temp.split(',')
for i in range(len(coin_num0)):
coin_num.append(int(coin_num0[i]))
s += coins[i] * coin_num[i]
n_map = {
'1': 'One',
'2': 'Two',
'3': 'Three',
'4': 'Four',
'5': 'Five',
'6': 'Six',
'7': 'Seven',
'8': 'Eight',
'9': 'Nine',
'10': 'Ten'
}
while True:
time.sleep(0.2)
sum = float(input('Please enter the amount of change you want:'))
# 当输入的总金额比收银员的总金额多, 无法进行找零
if sum > s:
print('The input amount is too large!')
continue
s = s - sum
i = len(coins) - 1
while i >= 0:
if sum >= coins[i]:
n = int(sum / coins[i])
if n >= coin_num[i]:
n = coin_num[i]
# 关键, 令sum动态改变
sum -= n * coins[i]
print('%s %s-dollar COINS were used'%(n_map.get(str(n)), n_map.get(str(coins[i]))))
i -= 1
break
活动安排问题
活动安排问题是解决需要共享公共资源的一系列活动的高效安排问题,以在限定的资源前提下尽可能多地安排活动
问题描述
有若干个活动,第i个活动的开始时间和结束时间是[Si, fi),只有一间教室,活动之间不能交叠,求最多安排多少个活动?
考虑集中贪心策略:
-
开始最早的活动优先,目的是想尽早结束活动,让出教室,但最早的活动可能时间最长,影响后面的活动
若活动开始和结束时间为: [0, 100),[1, 2),[2, 3),[3, 4),[4, 5] 安排[0,100)后,无法安排其他活动 # 最优解是安排除它之外的4个活动
-
短活动优先,但也可能出现反例
若活动开始和结束时间为: [0, 5),[5, 10),[3, 7) 这里[3, 7)最短,若安排了该活动,其他活动就无法安排了 # 最优解是安排其他两个
-
最少冲突活动优先
[0, 2) [2, 4) [4, 6) [6, 8) [1, 3) [1, 3) [1, 3) [3, 5) [5, 7) [5, 7) [5, 7) [0, 2)和3个活动冲突 [2, 4)和4个活动冲突 [4, 6)和3个活动冲突 [6, 8)和3个活动冲突 反过来, [1, 3)和[5, 7)每个都和5个活动冲突,而[3, 5)只有和两个活动冲突 按照策略,优先安排[3, 5),选择的[3, 5),最多只可能安排3个活动 但明显,第一行可以安排4个活动
再看第一种策略,看似不对,反而有效。
选出的活动自然是按照结束时间排好顺序,并且也都是不冲突的,假设排好的顺序为
a(1),a(2),a(3),...,a(m)
则按照贪心策略也可以如此排序
b(1),b(2),b(3),...,b(m)
假设,a(1)=b(1), a(2)=b(2), a(3)=b(3),...,a(k)=b(k),
将a(k+1)换成b(k+1),从而得到最优和贪心得到最多的一个相同,一个个替换,把最优解换成贪心策略的解。
代码实现
通过比较下一个说动的开始时间与上一个活动的结束时间的大小关系,确定这两个活动是否相容的。
若开始时间大于结束时间,则相容,反之不相容。
def bubble_sort(s, f):
for i in range(len(f)):
for j in range(len(f)-i-1):
if f[j] > f[j+1]:
f[j], f[j+1] = f[j+1], f[j]
s[j], s[j+1] = s[j+1], s[j]
return s, f
def greedy(s, f, n):
a = [True] * len(range(n))
x = 0
for i in range(1, n):
if s[i] >= f[x]:
a[i] = True
x = i
else:
a[i] = False
return a
# print("Separated by ','")
# arr = input().split()
arr = ['0, 100', '1, 2', '2, 3', '3, 5', '4, 5', '6, 8']
# print('Please enter the field number arrangement:')
# n = int(input())
n = 6
s = []
f = []
for i in arr:
# i = i[1: -1]
start = int(i.split(',')[0])
end = int(i.split(',')[1])
s.append(start)
f.append(end)
s, f = bubble_sort(s, f)
A = greedy(s, f, n)
res = []
for i in range(len(A)):
if A[i]:
res.append(f'({s[i]},{f[i]})')
print(''.join(res))
### >>>
"""
(1,2)
(1,2)(2,3)
(1,2)(2,3)(3,5)
(1,2)(2,3)(3,5)
(1,2)(2,3)(3,5)(6,8)
(1,2)(2,3)(3,5)(6,8)
"""
哈夫曼编码
哈夫曼编码是一种字符编码方式,可以对指定的字符集进行数据压缩,压缩率在20%~90%
问题描述
现有一个包含5个字符的字符表,各个字符出现的频率不同,需要构建一种有效率的编码类型,使用该编码表达这些字符表内容可以产生平均长度最短的位串。
在对于n个字符组成的文本进行编码的过程中,有两种编码方式:定长编码,变长编码
通常情况下,与定长编码相比,变长编码可以有效的减少表示同一字符集所需的编程长度,提升编码效率
但是,为了使用变长编码策略,需要解决在定长编码模式下不会遇到的问题,就是前缀码问题。
哈夫曼树
为了对某字母表构建一套二进制的前缀码,可以借助二叉树。将树中所有的左向边都标记为0,所有的右向边都标记为1,通过记录从根节点到字符所在的叶子节点的简单路径上的所有0-1标记来获得表示该字符的编码
根据二叉树的性质,从一个叶子节点到另一个叶子节点的简单路径不存在,所以字符集中的每个字符,其对应的编码不可能是其他字符的前缀,因此,任何一颗二叉树均能生成一套前缀码。
用贪心算法实现这个目标,算法由戴维·哈夫曼发明,故此为哈夫曼树
具体算法:
- 初始化n个单节点的树,并为它们标上字母表中的字符。把每个字符出现的频率记在其对应的根节点中,用来标记各个树的权重,即树的权重等于树中所有叶子节点的概率之和
- 重复一下步骤,直到只剩一颗单独的树,找到两颗权重最小的树,若两颗树权重相同,可任选其一,分别把它们作为新二叉树的左右子树,并把权重之和作为新的权重记录在新树的根节点中。
- 用上述算法构造的二叉树即为哈夫曼树。
代码实现
# Huffman Encoding
# Tree-Node Type
class Node:
def __init__(self, freq):
self.left = None
self.right = None
self.father = None
self.freq = freq
def is_left(self):
return self.father.left == self
# 创建叶子节点
def create_nodes(freqs):
return [Node(freq) for freq in freqs]
# Huffman 树
def create_Huffman_tree(nodes):
queue = nodes[:]
while len(queue)> 1:
queue.sort(key=lambda i: i.freq)
node_left = queue.pop(0)
node_right = queue.pop(0)
node_father = Node(node_left.freq + node_right.freq)
node_father.left = node_left
node_father.right = node_right
node_left.father = node_father
node_right.father = node_father
queue.append(node_father)
queue[0].father = None
return queue[0]
# Huffman 编码
def huffman_encoding(nodes, root):
codes = [''] * len(nodes)
for i in range(len(nodes)):
node_tmp = nodes[i]
while node_tmp != root:
if node_tmp.is_left():
codes[i] = '0' + codes[i]
else:
codes[i] = '1' + codes[i]
node_tmp = node_tmp.father
return codes
if __name__ == '__main__':
# chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N']
# freqs = [10, 4, 2, 5, 3, 4, 2, 6, 4, 4, 3, 7, 9, 6]
chars_freqs = [
('C', 2), ('G', 2), ('E', 3), ('K', 3), ('B', 4),
('F', 4), ('I', 4), ('J', 4), ('D', 5), ('H', 6),
('N', 6), ('L', 7), ('M', 9), ('A', 10)
]
nodes = create_nodes([i[1] for i in chars_freqs])
root = create_Huffman_tree(nodes)
codes = huffman_encoding(nodes, root)
for i in zip(chars_freqs, codes):
print(f'Character: {i[0][0]}, Freq: {i[0][1]}, Encoding: {i[1]}')