
在图论的世界里,最小生成树(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 sysdef prim(graph):num_vertices = len(graph)# 记录每个顶点是否已经加入生成树visited = [False] * num_vertices# 记录每个顶点到生成树的最小权值key = [sys.maxsize] * num_vertices# 记录每个顶点的父节点parent = [-1] * num_vertices# 从顶点 0 开始key[0] = 0for _ in range(num_vertices):# 选择距离生成树最近的顶点min_key = sys.maxsizemin_index = -1for 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] * ndef 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_xelif self.rank[root_x] < self.rank[root_y]:self.parent[root_x] = root_yelse:self.parent[root_y] = root_xself.rank[root_x] += 1return Truereturn Falsedef 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 = 0for edge in edges:u, v, weight = edgeif disjoint_set.union(u, v):mst_edges.append(edge)total_weight += weight# 输出最小生成树的边for edge in mst_edges:u, v, weight = edgeprint(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 算法可能更高效。通过深入理解这两种算法的贪心思想,我们可以更好地解决相关的实际问题。