目录
一个贪心算法总是做出当前最好的选择,也就是说,它期望通过局部最优选择从而得到全局最优的解决方案。
贪心算法只根据已有的信息做出选择,而且一旦做出了选择,不管将来有什么结果,这个选择都不会改变。换言之,贪心算法并不是从整体最优考虑,它所做的选择只是在某种意义上的局部最优。贪心算法能够得到许多问题的整体最优解或整体最优解的近似解。
在贪心算法中,需要注意以下问题:
1.一旦做出选择,不可以反悔
2.有可能得到的不是最优解,而是最优解的近似解
3.选择什么样的贪心策略,直接决定算法的好坏
通过贪心算法可以求解的问题通常具有两个重要的特性: 贪心选择性质和最优子结构性质。
(1)贪心选择
所谓贪心选择性质是指原问题的整体最优解可以通过一系列局部最优的选择得到。应用同一规则,将原问题变为一个相似的但规模更小的子问题,而后的每一步都是当前最佳的选择。运用贪心策略解决的问题在程序的运行过程中无回溯过程。
(2)最优子结构
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。
2.2 最优装载问题
问题描述要求装载的物品的数量尽可能多,而船的容量是固定的,那么优先把重量小的物品放进去,在容量固定的情况下,装的物品最多。
算法设计:
1.当载重量为定值c时,Wi越小时,可装载的古董数量n越大。只要依次选择最小重量古董,直到不能再装为止。
2.把n个古董的重量从小到大(非递减)排序,然后根据贪心策略尽可能多的选出前i个古董,直到不能继续装为止,此时达到最优。
算法实现:
"""
最优装载量,在总量一定的情况下,怎样能够装入更多的物品
"""
cur = 0
total = 0
total_weight, cargo_counts = map(int, input().split())
weight = []
# 填充货物的重量
weight.extend(map(int, input().split()))
weight.sort() # 按照重量排序
for i in range(cargo_counts):
cur += weight[i] # 当前的装载量
if cur <= total_weight:
total += 1
else:
break
print("总共装入:{:d}件物品".format(total))
2.3 背包问题
问题描述山洞中有n种宝物,每种宝物有一定重量w和相应的价值v,毛驴运载能力有限,只能运走m重量的宝物,一种宝物只能拿一样,宝物可以分割。那么怎么才能使毛驴运走宝物的价值最大呢?
算法设计:
每次从剩下的薄雾中选择性价比最高的宝物。
算法实现:
"""
背包问题:在总量一定的情况下,怎样能够装入的价值最高,可分割
"""
cargo_counts, total_weight = map(int, input().split())
sum_value = 0
cargo = []
# value = [0 for i in range(cargo_counts)] # 宝物的价值
# weight = [0 for i in range(cargo_counts)] # 宝物的重量
# cost_performance = [0 for i in range(cargo_counts)] # 宝物的性价比
for i in range(cargo_counts): # 输入宝物的信息
cargo.append(tuple(map(int, input().split()))) # 重量, 价值
cargo.sort(key=lambda a: a[1] / a[0], reverse=True)
for item in cargo:
if item[0] <= total_weight: # 能够全装下
sum_value += item[1]
total_weight -= item[0]
else: # 装部分
sum_value += item[1] / item[0] * total_weight
break
print("总装入的价值为:{}".format(sum_value))
2.4 会议安排
问题分析,会议安排的目的是能在有限的时间内召开更多的会议(任何两个会议不能同时进行)。在会议安排中,每个会议i都有起始时间bi和结束时间ej,且bi < ej,即一个会议进行的时间为半开区间[bi, ei)。如果[bi, ei)与[bj, ej)均在有限的时间内,且不相交,则称会议i与会议j相容的。会议安排问题要求在所给的会议集合中选出最大的相容活动子集,即尽可能在有限的时间内召开更多的会议。
算法思想:
每次从剩下的会议中选择具有最早结束时间且与已安排的会议相容的会议安排。
算法实现:
"""
会议安排:在有限时间内尽可能安排更多的会议
"""
meets = []
total_meets = int(input())
for i in range(total_meets):
meets.append(tuple(map(int, input().split()))) # 开始时间, 结束时间
meets.sort(key=lambda x: (int(x[1]), int(x[0]))) # 按照结束时间从小到大排序
last = 0 # 结束时间
total = 0 # 选择的会议数
for item in meets:
if last < item[0]: # 前一个会议的结束时间小于当前会议的开始时间
last = item[1]
total += 1
print("总共选择{}种会议".format(total))
2.5 最短路径
问题描述:
一个求单源最短路径的问题。给定有向带权图G=(V, E), 其中每条边的权是非负实数。此外,给定v中的一个顶点,称为源点。现在要计算从源到所有其他各顶点的最短路径长度,这里路径长度指路上各边的权之和。
算法思想:
首先假定源点为u,顶点集合v被划分为两部分: 集合S和集合V-S。初始时S中仅有源点u, 其中S中的顶点到源点的最短路径已经确定。集合V-S中所包含的顶点到源点的最短路径的长度待定,称从源点出发只经过S中的点到达V-S中的点的路径为特殊路径,并用数组dist[]记录当前每个顶点所对应的最短特殊路径长度。
(1)数据结构。设置地图的带权邻接矩阵为map[][], 即如果从源点u到顶点i有边,就令map[u][i]等于<u, i>的权值,否则map[u][i] = 无穷大;采用一维数组dist[i]来记录从源点到i顶点的最短路径长度;采用一维数组p[i]来记录最短路径上i顶点的前驱。
(2)初始化。令集合S={u}, 对于集合V-S中的所有顶点x,初始化dist[i] = map[u][i], 如果源点u到顶点i有边相连,初始化p[i] = u, 否则p[i] = -1。
(3)找最小。在集合V-S中依照贪心策略来寻找使得dis[j]具有最小值的顶点t,即dist[t] = min(dist[j])(j属于V-S集合), 则顶点t就是集合V-S中距离源点u最近的顶点。
(4)加入S战队。将顶点t加入集合S中,同时更新V-S。
(5)判结束。如果集合V-S为空,算法结束,否则转(6)。
(6)借东风。在(3)中已经找到了源点到t的最短路径,那么对集合V-S中所有与顶点t相邻的顶点j,都可以借助t走捷径。如果dist[j] > dist[t] + map[t][j], 则dist[j] = dist[t] + map[t][j], 记录顶点j的前驱为t, 有p[j] = t, 转(3)
由此,可求得从源点u到图G的其余各个顶点的最短路径及长度,也可通过数组p[]逆向找到最短路径上通过的城市。
算法实现:
""""
最短路径: 迪杰斯特拉算法计算单源最短路径
"""
city_counts, road_counts = map(int, input().split())
graph = [[float('inf') for j in range(city_counts + 1)] for i in range(city_counts + 1)] # 保存图
book = [0 for i in range(city_counts + 1)] # 记录顶点是否已包含
def find_node(): # 寻找当前最优节点
min = float('inf')
key = None
for i in range(1, city_counts+1):
if book[i] == 0 and min > dist[i]:
min = dist[i]
key = i
return key
for i in range(road_counts): # 读入边
u, v, w = map(int, input().split())
graph[u][v] = w
start = int(input()) # 输入起点
dist = [float('inf') for i in range(city_counts + 1)] # 保存源点到顶点的最短距离
dist[start] = 0
book[start] = 1 # 将节点加入
parents = [-1 for i in range(city_counts + 1)] # 保存顶点的父节点
for i in range(1, city_counts + 1): # 初始化单源
if graph[start][i] < float('inf'):
dist[i] = graph[start][i]
parents[i] = start
for i in range(1, city_counts):
node = find_node()
book[node] = 1
for j in range(1, city_counts+1):
if dist[j] > dist[node] + graph[node][j]:
dist[j] = dist[node] + graph[node][j]
parents[j] = node
for i in range(1, city_counts+1):
print(dist[i])
2.6 霍夫曼编码
问题描述:
不等长编码方法需要解决两个关键问题 (1)编码尽可能短 (2)不能有二义性。(任何一个字符的编码不能是另一个字符编码的前缀,即前缀码特性)。
算法思想:
假设有n个权值, 则构造出的huffman树有n个叶子节点。n个权值分别设为w1、w2、…、wn, 则huffman树的构造规则为:
1. 将w1、w2、…、wn看成是有n棵树的森林(每棵树仅有一个结点);
2. 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左右子树,且新树的根节点权值为左右子树结点权值之和;
3. 从森林中删除选取的两棵树,并将新树加入森林;
4. 重复(2)、(3)步,直到森林中只剩下一棵树为止,该树即为所求得的huffman树。
为了使得到的哈夫曼树的结构尽量唯一,通常规定生成的哈夫曼树中每个结点的左子树根结点的权小于等于右子树根结点的权。
算法实现:
"""
huffman树
假设有n个权值, 则构造出的huffman树有n个叶子节点。n个权值分别设为w1、w2、…、wn, 则huffman树的构造规则为:
1. 将w1、w2、…、wn看成是有n棵树的森林(每棵树仅有一个结点);
2. 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左右子树,且新树的根节点权值为左右子树结点权值之和;
3. 从森林中删除选取的两棵树,并将新树加入森林;
4. 重复(2)、(3)步,直到森林中只剩下一棵树为止,该树即为所求得的huffman树。
为了使得到的哈夫曼树的结构尽量唯一,通常规定生成的哈夫曼树中每个结点的左子树根结点的权小于等于右子树根结点的权。
"""
class Node(object):
"""树的节点数据结构"""
def __init__(self, name, weight):
self.name = name
self.weight = weight
self.parent = None
self.lchild = None
self.rchild = None
def __str__(self):
return self.name + ' ' + str(self.weight)
# Name_set = [] # 保存当前的n个节点名字
code_dict = {}
Node_list = [] # 保存当前的n个结点
total = int(input())
for i in range(total): # 读入n个结点
name, weight = input().split()
# Name_set.append(name)
tmp = Node(name, float(weight))
# Name_set.append(name) # 将节点名字添加到未处理的集合中
Node_list.append(tmp) # 将结点添加到列表中
def find_least_node(name): # 寻找两棵最小结点合并并进行后续操作
if len(Node_list) > 1:
Node_list.sort(key=lambda x: x.weight) # 0为最小 1为次小
tmp = Node(chr(ord(name) + 1), Node_list[0].weight + Node_list[1].weight)
tmp.lchild = Node_list[0] # 添加子节点
tmp.rchild = Node_list[1]
Node_list[0].parent = tmp # 添加父节点
Node_list[1].parent = tmp
# if Node_list[0].name in Name_set:
# l.append(Node_list[0])
# if Node_list[1].name in Name_set:
# l.append(Node_list[1])
# 添加删除操作
Node_list.append(tmp)
Node_list.pop(0)
Node_list.pop(0)
return tmp.name
# 构建huffman树的过程
name = find_least_node(name)
while name is not None:
name = find_least_node(name)
code = []
def get_code(node, code): # 先序遍历树
if node.lchild is None and node.rchild is None: # 叶节点
code_dict[node.name] = ''.join(code)
return
code.append('0')
get_code(node.lchild, code)
code.pop(-1)
code.append('1')
get_code(node.rchild, code)
code.pop(-1)
get_code(Node_list[0], code)
# for item in l:
# print(item)
for key, value in code_dict.items():
print(key, value)
2.7 最小生成树
问题描述:
将各个单位抽象为图中的顶点,顶点与顶点之间的边表示单位之间的通信网络,边的权值表示布线的费用。如果两个节点之间没有连线,代表这两个单位之间不能布线,费用为无穷大。那么如何设计网络电缆布线,将各个单位联通起来,并且费用最少。
概念解释: (1)子图,从原图中选中一些顶点和边组成的图,称为原图的子图
(2) 生成子图:选中一些边和所有顶点组成的图,称为原图的生成子图
(3) 生成树: 如果生成子图恰好是一棵树,则称为生成树
(4) 最小生成树: 权值之和最小的生成树,则称为最小生成树。
算法思想:
只需要在两个集合(V和V-U)的边中选择一条权值最小,把权值最小的边关联的结点加入到集合U。并更新相应结点距离。重复n-1,最终构造一棵最小生成树。
算法实现:
"""
prim最小生成树算法:
1. 从任意一个顶点开始构造生成树, 假设就从1号顶点开始。首先将顶点1加入生成树中,用一个数组book来标记哪些顶点已经加入生成树
2. 用数组dis记录生成树到各个顶点的距离。最初生成树中只有1号顶点,有直连边时,数组dis中存储的就是1号顶点到该顶点的边的权值,没有直连边的时候就是无穷大。即初始化dis数组
3. 从数组dis中选出离生成树最近的顶点(假设这个顶点为j)加入到生成树中(即在数组dis中找最小值)。再以j为中间点,更新生成树到每一个非树顶点的距离(就是松弛),即如果dis[k] > e[j][k],则更新dis[k]=e[j][k]
4. 重复第3步,直到生成树中有n个顶点为止。
"""
n, m = map(int, input().split()) # n个顶点, m条边
count = 0 # 记录纳入生成树的节点数
sum = 0 # 总的路径长度
graph = {} # 路径图
dis = [float('inf') for i in range(n+1)] # 记录到达生成树的最短距离
dis[1] = 0 # 1节点初始化为0
book = [0 for i in range(n+1)] # 记录
# 初始化
for i in range(1, n+1):
graph[i] = dict()
for i in range(m): # 读入边
u, v, w = map(int, input().split()) # 起点, 终点, 权值
graph[u].update({v: w}) # 存储图
graph[v].update({u: w})
for key, value in graph[1].items():
dis[key] = value
book[1] = 1 # 将节点1加入生成树
count += 1
while count < n:
min = float('inf')
for i in range(n+1):
if book[i] == 0 and dis[i] < min:
min = dis[i]
j = i
# 找出了当前到生成树距离最近的节点
book[j] = 1
count += 1
sum += dis[j]
# 根据新找的节点更新距离
for i in graph[j]:
if book[i] == 0 and dis[i] > graph[j][i]:
dis[i] = graph[j][i]
print(sum)
# print(graph)