接下来我们看最小生成树问题,和下一节的最短路径问题容易混淆。
最小生成树的概念
(1)一个带权值的图:网。并需要最小成本,就是用n-1条边把n个顶点连接起来,且连接起来的权值最小。
(2)我们把构造联通网的最小代价生成树称为最小生成树
产生最小生成树必须解决下边两个问题:(1) 尽可能选取权值小的边,但不能构成回路;(2) 选取n-1条恰当的边以连通n个顶点。
最小生成树的算法主要有Kruskal算法和Prim算法,他们都是贪心算法的应用。
最小生成树的应用
用带权值的图表示一群城市及城市间的距离,那么最小生成树可以理解为该城市群中修建铁路、公路的总成本。最小生成树的应用基本类似这样,而最短路径用于两个城市之间的一些成本的计算。
最小生成树的算法
下面我们以下图为例,撸一遍最小生成树的算法。
对于这幅图,我们给的已知条件如下。
n = 9
edge = [(0,1,10), (0,5,11), (1,2,18),(1,6,16), (1,8,12), (2,3,22),(2,8,8), (3,4,20), (3,6,24), (3,7,16), (3,8,21), (4,5,26), (4,7,7), (5,6,17), (6,7,19)]
(1) Prim算法
过程描述:Prim算法始终以顶点为主导,并且起始点的选择是任意的。
从起始点到其他点选择最小权值边,然后以此边两个顶点分别再找最小权值的边,同样已经间接连接的边跳过。
时间复杂度是O(n2),适用于求边稠密连通网的最小生成树。
结合图和代码的注释,学习一个。解法的关键是lowcost的更新,然后贪心的选择最优的边及其顶点,思路还是很简单的。
def miniSpanTree_prim(n,edge):
from collections import defaultdict
# 根据边 建图
graph = defaultdict(dict)
for e in edge:
graph[e[0]][e[1]] = e[2]
graph[e[1]][e[0]] = e[2]
# 注意这两个工具, lowcost存储当前情况下(初始为v0) 所有顶点的最小权重
# prenodes 为所有顶点最小权重对应的上一个顶点
lowcost = [0 for i in range(n)]
prenodes = [0 for i in range(n)]
# 初始化lowcost : 所有顶点和v0的距离 没有边的话 可以设置为一个不可能的值
for i in range(1, n):
lowcost[i] = graph[0].get(i, -1)
cost = 0 # 最小生成树总成本
for i in range(1, n): # i代表迭代次数,无意义
minw = 999
j = 1
val = 0
# 遍历所有顶点 找出lowcost中 权重最小的,和本身的距离为0 不考虑,没有边时为-1 不考虑
while j < n:
if lowcost[j] > 0 and lowcost[j] < minw:
minw = lowcost[j]
val = j
j += 1
print(prenodes[val],"->", val, minw) # 此时确定了当前情况下选择的下一个顶点val即边的权重
lowcost[val] = 0 # lowcost中设置为0 表示val已被选择
cost += minw
# 遍历一遍 val顶点的所有边 更新(!!)其他顶点的最小权重 和 prenodes
for k in range(1, n):
if k not in graph[val].keys():
continue
if lowcost[k] != 0:
if lowcost[k] > 0 and graph[val][k] < lowcost[k] or lowcost[k] == -1:
lowcost[k] = graph[val][k]
prenodes[k] = val
return cost
我们以0点为初始,运行代码结果如下:
miniSpanTree_prim(n, edge)
'''
0 -> 1 10
0 -> 5 11
1 -> 8 12
8 -> 2 8
1 -> 6 16
6 -> 7 19
7 -> 4 7
7 -> 3 16
99
'''
(2) Kruskal算法
过程描述:始终以边为主导地位,先选择权值最小的边,总是选择当前可用最小权值边,并且每次判断两点之间是否已经间接连通,如果已经间接连通,则跳过此边。
时间复杂度是O(n*logn),适用于求边稀疏连通网的最小生成树。
还是要结合图和代码,自己画一遍中间变量 尤其是prenodes,理解算法的精髓。该算法核心就是理解prenodes用于判断是否成环,以及他的更新,,代码是Kruskal的核心,还可以根据需要输出更多信息。
def find_parent(x, info):
while info[x] > 0:
x = info[x]
return x
def miniSpanTree_kruskal(n, edge):
prenodes = [0 for i in range(n)] # 核心变量
edge.sort(key=lambda x: x[2]) # 边按照权重排序
print(edge)
cost = 0
for e in edge:
# 查找 这条边两顶点的父节点 下面列出了 begin和end在前几次循环过程的值
begin = find_parent(e[0], prenodes) # 4 2 0 1 5 3
end = find_parent(e[1], prenodes) # 7 8 1 5 8 7
# 说明没有回路
if begin != end:
prenodes[begin] = end # 此时将该条边加入最小生成树 更新prenodes
print(e[0], "->", e[1], e[2])
cost += e[2]
return cost
代码运行结果如下:
miniSpanTree_kruskal(n, edge)
'''
[(4, 7, 7), (2, 8, 8), (0, 1, 10), (0, 5, 11), (1, 8, 12), (1, 6, 16), (3, 7, 16), (5, 6, 17), (1, 2, 18), (6, 7, 19), (3, 4, 20), (3, 8, 21), (2, 3, 22), (3, 6, 24), (4, 5, 26)]
4 -> 7 7
2 -> 8 8
0 -> 1 10
0 -> 5 11
1 -> 8 12
1 -> 6 16
3 -> 7 16
6 -> 7 19
99
'''