目录
一,算法的总体思想
贪心算法总是选择当前局面的最优解,也就是说贪心算法不从全局最优解来考虑,它做出的选择是在某种意义上的局部最优。在很多情况下贪心算法得出的结果是最优解的一个近似。
下面有一个硬币找零的例子:
假设有四种硬币:二角五分、一角、五分和一分。现要找给顾客六角三分。 显然,会拿出2个二角五分、1个一角和3个一分的硬币交给顾客。
贪心方法思路:首先选出一个面值不超过六角三分的最大硬币,即二角五分,然后在剩余数中再选最大面值的硬币,依此类推,得到其解。
若硬币面值改为:
一角一分、五分和一分,而要找给顾客一角五分钱。 用贪心算法将找给1个一角一分和4个一分的硬币。然而,3个五分硬币是最好的找法。
此时,贪心算法没有得到整体最优解。但通常可得到最优解的很好近似。
二,贪心算法的基本要素
1,最优子结构性质
2,贪心选择性质
所求问题的整体最优可以通过一系列的局部最优解来达到
这是贪心算法与动态规划的主要区别:
动态规划将问题分解为可以解决的子问题,从最小规模的问题开始求解,逐步解得更大规模的问题,是自下而上的求解;贪心选择是自上向下的,迭代地进行贪心选择,每做一次贪心选择问题的规模就会缩小一部分,从而得到将问题变成规模更小的子问题。
贪心算法证明时,通常使用替换法。
三,贪心算法的一些例子
1,活动安排问题
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。
每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi, 且si <fi 。
如果选择了活动i,则它在[si, fi)内占用资源。
若[si, fi)与[sj, fj)不相交,则称活动i与j是相容的,即当si≥fj或sj≥fi时,活动i与活动j相容。
活动安排问题就是求E的最大相容活动子集
问题分析:
某活动结束时间越早,则留给其他活动的时间就越长,因此贪心选择算法每次选择结束时间最早的活动。
替换法证明 :
设最优排序 S = a, b, c
贪心选择出的 a' 是全局结束时间最早的活动,因此 S' = a', b, c 肯定满足时间要求,则 S' 也是最优解。
def greedy_selector(n, s, f) :
s = [int(ele) for ele in s]
f = [int(ele) for ele in f]
items = list(range(1, n + 1))
sort_sf = sorted(zip(s, f, items), key = lambda x : x[1])
s_sorted, f_sorted, items_sorted = zip(*sort_sf)
ans = []
cnt = 0
finish_time = 0
for i in range(n) :
if s_sorted[i] >= finish_time :
ans.append(items_sorted[i])
finish_time = f_sorted[i]
cnt += 1
return ans, cnt
n = int(input())
s = input().split()
f = input().split()
ans, cnt = greedy_selector(n, s, f)
print(ans)
print(cnt)
# 输入
11
1 3 0 5 3 5 6 8 8 2 12
4 5 6 7 8 9 10 11 12 13 14
# 输出
[1, 4, 8, 11]
4
2,Huffman编码问题
在计算机中,所有的数据传输(以字符串为例)都是通过二进制的形式进行的,因此,对出现频率较高的字符赋予长度较短的二进制码,可以提高数据传输的效率,为避免歧义,需要每一个字符的二进制编码都不是其他字符的前缀码,这时便可以通过树来实现,对于二叉树的非根节点而言,若左节点赋值为0,右节点赋值为1,则所有叶子节点所代表的值即符合要求,即Huffman树。
首先需要统计各个字符在字符串中出现的次数放入优先队列,可以通过字典记录,并根据出现的次数升序排序,将当前队列中频次最少的两项合成为一个二叉树,并将其父节点赋值为二者频次的和,插入到上述队列中,重复上述过程,直到优先队列中只有一个节点,此时Huffman树便构建完成了。压缩时,根据Huffman树中的编码将所给字符串依次转换为二进制字符,解压时将二进制字符依次转换为相应字符,即可完成传输。
关于代码实现:
首先构建Huffman节点:
class HuffmanNode:
def __init__(self, frequency, value=None):
self.frequency = frequency
self.value = value
self.left = None
self.right = None
def __lt__(self, other): # 重载运算符,用于优先队列的构建
return self.frequency < other.frequency
节点中存储有当前字符value和字符出现的频率frequency,并创建了左右子节点,将其指向None。
有了Huffman节点后,开始构建Huffman树:
def build_huffman_tree(data):
# 首先统计各字符出现的次数
frequency_map = defaultdict(int)
for char in data:
frequency_map[char] += 1
# 创建优先队列,并按照出现频率升序排序
priority_queue = []
for char, frequency in frequency_map.items():
node = HuffmanNode(frequency, char)
heapq.heappush(priority_queue, node)
# 构造Huffman树并返回树的根节点
while len(priority_queue) > 1:
left_node = heapq.heappop(priority_queue)
right_node = heapq.heappop(priority_queue)
parent_node = HuffmanNode(left_node.frequency + right_node.frequency)
parent_node.left = left_node
parent_node.right = right_node
heapq.heappush(priority_queue, parent_node)
return heapq.heappop(priority_queue)
对Huffman树进行编码,并返回得到的映射规则
def encode_huffman_tree(node, prefix='', encoding_map = None):
if encoding_map is None:
encoding_map = {}
if node.value:
encoding_map[node.value] = prefix
else:
encode_huffman_tree(node.left, prefix + '0', encoding_map)
encode_huffman_tree(node.right, prefix + '1', encoding_map)
return encoding_map
代码中,如果value的值不为None,即此时为叶子节点,则将编码prefix赋予value;
若value的值为None,则还没有达到叶子节点,将prefix + ‘0’,访问左子节点,抑或是将
prefix + ‘1’访问右子节点。
按照得到的编码规则对所给字符串进行compress
def compress(data, encoding_map):
compressed_data = ''
for char in data:
compressed_data += encoding_map[char]
return compressed_data
按照Huffman树对得到的编码进行解码
def decompress(compressed_data, huffman_tree):
current_node = huffman_tree
decompressed_data = ''
for bit in compressed_data:
if bit == '0':
current_node = current_node.left
else:
current_node = current_node.right
if current_node.value:
decompressed_data += current_node.value
current_node = huffman_tree
return decompressed_data
以下是输入与结果
data = "Hello World!"
huffman_tree = build_huffman_tree(data)
encoding_map = encode_huffman_tree(huffman_tree)
compressed_data = compress(data, encoding_map)
decompressed_data = decompress(compressed_data, huffman_tree)
print("映射规则:",encoding_map)
print("原始数据:", data)
print("压缩后的数据:", compressed_data)
print("解压后的数据:", decompressed_data)
# 输出为:
映射规则: {'r': '000', '!': '001', 'l': '01', 'd': '100', 'o': '101', 'e': '1100', ' ': '1101', 'H': '1110', 'W': '1111'}
原始数据: Hello World!
压缩后的数据: 1110110001011011101111110100001100001
解压后的数据: Hello World!
3,最小生成树
设G = (V, E)是一个无向连通带权图,即一个网络。E的每条边(v, w)的权为c[v][w]。
如果G的一个子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。
生成树的各边权的总和称为该生成树的耗费。
在G的所有生成树中,耗费最小的生成树称为G的最小生成树MST(minimum spanning tree) 。
最小生成树的贪心选择性质:
不断从当前已有的选择中,找到符合条件的最优解,直到构造出最小生成树,树是由顶点和边构成的,而最小生成树的边会比顶点少一个,从这两个方面出发可以有不同的贪心选择策略
Prim算法:
从顶点出发,创建已选顶点集S,未选顶点集K,任选一顶点 i 初始化S,在K中找到顶点 j 使得边(i, j)最小,注意 i 是已选顶点集S中,j 是未选顶点集K中。将 j 加入S,并从K中去除。
边集用c[ i ][ j ]给出,c 是边的权重。若顶点 i 与顶点 j 之间没有边相连,则c[ i ][ j ] = -1
图如下:
抽象为矩阵:
def Prim(c, n) :
ans = []
S = [1]
K = list(range(2, n + 1))
sum = 0
se = float('inf') # select 用于存储当前最小权重
sec = [] # select 用于存储最小权重对应的边
while len(K) > 0 : # K不为空,算法继续
for i in S : # 在已选集中选择顶点i作为起点
for j in K : # 在未选集中选择顶点j作为终点
if c[int(i)][int(j)] == -1 : # 若(i, j)没有边连接,则跳过
continue
else :
if se > c[int(i)][int(j)] :
se = c[int(i)][int(j)]
sec = [i, j]
S.append(int(sec[1]))
sum += se
K.pop(K.index(int(sec[1])))
ans.append(sec)
se = float('inf')
sec = []
print(sum)
return ans
下面是输入处理与运行结果
n = int(input())
c = []
for i in range(n + 1) :
line = list(map(int, input().split()))
c.append(line)
print(Prim(c, n))
# 输入:
6
0 1 2 3 4 5 6
1 -1 6 1 5 -1 -1
2 6 -1 5 -1 3 -1
3 1 5 -1 5 6 4
4 5 -1 5 -1 -1 2
5 -1 3 6 -1 -1 6
6 -1 -1 4 2 6 -1
# 输出:
15
[[1, 3], [3, 6], [6, 4], [3, 2], [2, 5]]
相同问题的Kruskal算法:
该算法从边出发,选择当前权值最小的边并判断是否会构成回路(若边的两个端点都在已选顶点集内,则构成回路),直到选择n - 1条边为止
该算法可以使用并查集实现,先将所有边的权重排序,从最小的开始选择,若一个边中两个节点的根节点一致,则代表两个节点之间已经构成回路,若根节点不同,则将其加入并查集中。
实现代码:
def find(parent, i) :
if parent[i] == i :
return i
return find(parent, parent[i])
def union(parent, rank, x, y) :
xroot = find(parent, x)
yroot = find(parent, y)
if rank[xroot] < rank[yroot] :
parent[xroot] = yroot
elif rank[xroot] > rank[yroot] :
parent[yroot] = xroot
else :
parent[yroot] = xroot
rank[xroot] += 1
def Kruskal(c, n) :
edges = []
for i in range(1, n + 1) :
for j in range(i + 1, n + 1) :
if c[i][j] != -1 :
edges.append((c[i][j], i, j))
edges.sort()
parent = [i for i in range(n + 1)]
rank = [0] * (n + 1)
ans = []
sum = 0
for edge in edges :
weight, x, y = edge
if find(parent, x) != find(parent, y) :
ans.append((x, y))
sum += weight
union(parent, rank, x, y)
return ans, sum
结果:
n = int(input())
c = []
for i in range(n + 1) :
line = list(map(int, input().split()))
c.append(line)
print(Kruskal(c, n))
# 输入:
6
0 1 2 3 4 5 6
1 -1 6 1 5 -1 -1
2 6 -1 5 -1 3 -1
3 1 5 -1 5 6 4
4 5 -1 5 -1 -1 2
5 -1 3 6 -1 -1 6
6 -1 -1 4 2 6 -1
# 输出:
([(1, 3), (4, 6), (2, 5), (3, 6), (2, 3)], 15)