在图论中,最小生成树(Minimum Spanning Tree,MST)是一个重要的概念。对于一个带权连通无向图,最小生成树是一棵包含图中所有顶点的树,并且树中所有边的权值之和最小。最小生成树在很多实际问题中都有广泛的应用,比如在通信网络的布线问题中,要使所有节点都连通且布线成本最低;在交通网络的规划中,要以最小的建设成本连接各个城市等。本文将详细介绍两种经典的最小生成树算法:Prim 算法和 Kruskal 算法。
Prim 算法是一种贪心算法,它从图中的任意一个顶点开始,逐步扩展生成树。每次选择一条连接已在生成树中的顶点和不在生成树中的顶点的边,且这条边的权值最小,将这条边和对应的顶点加入到生成树中,直到所有顶点都被加入到生成树中。
import sys
def prim(graph):
num_vertices = len(graph)
# 用于记录每个顶点是否已被加入到生成树中
visited = [False] * num_vertices
# 用于记录每个顶点到生成树的最小权值
key = [sys.maxsize] * num_vertices
# 用于记录每个顶点的父节点
parent = [-1] * num_vertices
# 选择第一个顶点作为起始顶点
key[0] = 0
for _ in range(num_vertices):
# 找到未被访问且权值最小的顶点
min_key = sys.maxsize
min_index = -1
for v in range(num_vertices):
if not visited[v] and key[v] < min_key:
min_key = key[v]
min_index = v
# 将该顶点标记为已访问
visited[min_index] = True
# 更新与该顶点相邻的顶点的权值和父节点
for v in range(num_vertices):
if graph[min_index][v] > 0 and not visited[v] and graph[min_index][v] < key[v]:
key[v] = graph[min_index][v]
parent[v] = min_index
# 输出最小生成树的边和权值
result = []
for i in range(1, num_vertices):
result.append((parent[i], i, graph[i][parent[i]]))
return result
# 示例图的邻接矩阵表示
graph = [
[0, 2, 0, 6, 0],
[2, 0, 3, 8, 5],
[0, 3, 0, 0, 7],
[6, 8, 0, 0, 9],
[0, 5, 7, 9, 0]
]
mst = prim(graph)
print("Prim 算法得到的最小生成树的边和权值:")
for edge in mst:
print(f"边: ({edge[0]} - {edge[1]}), 权值: {edge[2]}")
Kruskal 算法也是一种贪心算法,它的基本思想是将图中的所有边按照权值从小到大进行排序,然后依次选择权值最小的边,如果这条边加入到生成树中不会形成环,则将其加入到生成树中,直到生成树包含图中所有顶点。
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x]!= x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
root_x = self.find(x)
root_y = self.find(y)
if root_x!= root_y:
if self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
elif self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
return True
return False
def kruskal(graph):
num_vertices = len(graph)
edges = []
# 提取所有边
for i in range(num_vertices):
for j in range(i + 1, num_vertices):
if graph[i][j] > 0:
edges.append((i, j, graph[i][j]))
# 按权值从小到大排序
edges.sort(key=lambda x: x[2])
uf = UnionFind(num_vertices)
mst = []
for edge in edges:
u, v, weight = edge
if uf.union(u, v):
mst.append(edge)
return mst
# 示例图的邻接矩阵表示
graph = [
[0, 2, 0, 6, 0],
[2, 0, 3, 8, 5],
[0, 3, 0, 0, 7],
[6, 8, 0, 0, 9],
[0, 5, 7, 9, 0]
]
mst = kruskal(graph)
print("Kruskal 算法得到的最小生成树的边和权值:")
for edge in mst:
print(f"边: ({edge[0]} - {edge[1]}), 权值: {edge[2]}")
算法 | 算法思想 | 适用场景 | 时间复杂度 | 空间复杂度 |
---|---|---|---|---|
Prim 算法 | 从一个顶点开始,逐步扩展生成树,每次选择连接已在生成树中的顶点和不在生成树中的顶点的最小权值边 | 适用于稠密图(边的数量接近顶点数量的平方) | $O(V^2)$ 或 $O((V + E)\log V)$ | $O(V)$ |
Kruskal 算法 | 将所有边按权值排序,依次选择最小权值边,只要不形成环就加入生成树 | 适用于稀疏图(边的数量远小于顶点数量的平方) | $O(E\log E)$ | $O(E)$ |
Prim 算法和 Kruskal 算法是两种经典的最小生成树算法,它们都基于贪心策略,通过不同的方式逐步构建最小生成树。在实际应用中,需要根据图的特点(稠密图或稀疏图)来选择合适的算法。如果图是稠密图,Prim 算法可能更合适;如果图是稀疏图,Kruskal 算法可能更高效。通过理解和掌握这两种算法,我们可以解决很多与最小生成树相关的实际问题。