在图论的世界里,最小生成树(MST, Minimum Spanning Tree)是一个非常重要的概念。它在众多领域都有着广泛的应用,比如网络布线、交通规划等。为了求解最小生成树问题,人们提出了多种算法,其中 Prim 算法和 Kruskal 算法是基于贪心思想的经典算法。接下来,我们将深入探讨这两种算法的贪心思想以及它们的具体实现。
贪心算法是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法。贪心算法并不从整体最优上加以考虑,它所做出的仅是在某种意义上的局部最优选择。虽然贪心算法不能保证得到全局最优解,但对于很多问题,它可以得到最优解或者近似最优解。最小生成树问题就是贪心算法能够得到全局最优解的典型例子。
给定一个无向连通带权图 (G=(V, E)),其中 (V) 是顶点集合,(E) 是边集合。最小生成树是图 (G) 的一个子图,它是一棵包含图中所有顶点的树,并且所有边的权值之和最小。
Prim 算法从一个任意顶点开始,逐步扩展生成树,每次都选择与当前生成树相连的边中权值最小的边,将对应的顶点加入到生成树中,直到所有顶点都被包含在生成树中。其贪心策略是在每一步都尽可能选择权值最小的边来扩展生成树。
假设有一个包含 5 个顶点的无向连通带权图,其邻接矩阵如下:
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 0 | 2 | 3 | 0 | 0 |
1 | 2 | 0 | 4 | 7 | 0 |
2 | 3 | 4 | 0 | 5 | 6 |
3 | 0 | 7 | 5 | 0 | 8 |
4 | 0 | 0 | 6 | 8 | 0 |
我们从顶点 0 开始执行 Prim 算法:
import sys
def prim(graph):
num_vertices = len(graph)
# 记录每个顶点是否已经加入生成树
visited = [False] * num_vertices
# 记录每个顶点到生成树的最小权值
key = [sys.maxsize] * num_vertices
# 记录每个顶点的父节点
parent = [-1] * num_vertices
# 从顶点 0 开始
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
# 输出最小生成树的边
for i in range(1, num_vertices):
print(f"Edge: {parent[i]} - {i}, Weight: {graph[i][parent[i]]}")
# 输出最小生成树的权值之和
total_weight = sum(key)
print(f"Total weight of MST: {total_weight}")
# 测试代码
graph = [
[0, 2, 3, 0, 0],
[2, 0, 4, 7, 0],
[3, 4, 0, 5, 6],
[0, 7, 5, 0, 8],
[0, 0, 6, 8, 0]
]
prim(graph)
Kruskal 算法将图中的所有边按照权值从小到大进行排序,然后依次选择权值最小的边,如果这条边加入到生成树中不会形成环,则将其加入到生成树中,直到生成树包含所有顶点。其贪心策略是在每一步都选择权值最小且不会形成环的边。
还是以上面的图为例,将所有边按照权值从小到大排序:
| 边 | 权值 |
|—-|—-|
| ((0, 1)) | 2 |
| ((0, 2)) | 3 |
| ((1, 2)) | 4 |
| ((2, 3)) | 5 |
| ((2, 4)) | 6 |
| ((1, 3)) | 7 |
| ((3, 4)) | 8 |
依次选择边:
class DisjointSet:
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])
disjoint_set = DisjointSet(num_vertices)
mst_edges = []
total_weight = 0
for edge in edges:
u, v, weight = edge
if disjoint_set.union(u, v):
mst_edges.append(edge)
total_weight += weight
# 输出最小生成树的边
for edge in mst_edges:
u, v, weight = edge
print(f"Edge: {u} - {v}, Weight: {weight}")
# 输出最小生成树的权值之和
print(f"Total weight of MST: {total_weight}")
# 测试代码
graph = [
[0, 2, 3, 0, 0],
[2, 0, 4, 7, 0],
[3, 4, 0, 5, 6],
[0, 7, 5, 0, 8],
[0, 0, 6, 8, 0]
]
kruskal(graph)
算法 | 时间复杂度 | 适用场景 | 贪心策略 |
---|---|---|---|
Prim 算法 | (O(V^2))(邻接矩阵),(O((V + E) \log V))(邻接表) | 稠密图(边数较多) | 从一个顶点开始,每次选择与当前生成树相连的权值最小的边 |
Kruskal 算法 | (O(E \log E)) | 稀疏图(边数较少) | 对所有边排序,每次选择权值最小且不形成环的边 |
Prim 算法和 Kruskal 算法都是基于贪心思想的求解最小生成树问题的有效算法。它们的贪心策略不同,但都能得到全局最优解。在实际应用中,我们可以根据图的稠密程度选择合适的算法。如果图是稠密图,Prim 算法可能更合适;如果图是稀疏图,Kruskal 算法可能更高效。通过深入理解这两种算法的贪心思想,我们可以更好地解决相关的实际问题。