### 实验题14:求解建公路问题(图论算法)
#### 问题背景
在农村地区,为了实现村村通公路的目标,需要从多个候选道路中选择一部分进行修建,使得所有村庄都能互相连通,同时总建造成本最低。这是一个典型的最小生成树(Minimum Spanning Tree, MST)问题[^3]。
#### 解决方案概述
解决此类问题的核心在于寻找一种方法,能够在保证所有节点连通的前提下,选取权值和最小的一组边。常见的两种经典算法分别为 Prim 算法 和 Kruskal 算法。以下是这两种算法的具体介绍及其 Python 实现。
---
### 方法一:Kruskal 算法
#### 思想描述
Kruskal 算法是一种基于贪心策略的解决方案,其基本思路如下:
- 将所有的边按照权重从小到大排序;
- 初始化每个顶点为独立的一个集合;
- 按照顺序依次考虑每条边,如果这条边连接的两个顶点不属于同一个集合,则将其加入结果集中,并将这两个顶点所在的集合合并;
- 重复上述过程直到结果集中的边数量等于顶点数减一为止[^4]。
#### 时间复杂度
假设共有 $E$ 条边和 $V$ 个顶点,则 Krusakl 算法的主要开销来自于对边的排序 ($O(E\log E)$) 及 Union-Find 数据结构的操作 ($O(\alpha(V))$, 其中 $\alpha$ 是反 Ackermann 函数)[^4]。
#### Python 实现
```python
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # Path compression
return self.parent[x]
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
self.parent[rootY] = rootX
def kruskal(edges, N):
"""
使用 Krusakl 算法计算最小生成树
:param edges: 边列表 [(u,v,w)], 表示 u-v 之间有一条权值 w 的边
:param N: 节点总数
:return: 最小生成树的总权重 或 -1 如果无法形成一棵完整的树
"""
uf = UnionFind(N)
total_weight = 0
mst_edges = []
# Sort edges by weight
edges.sort(key=lambda e: e[2])
for edge in edges:
u, v, w = edge
if uf.find(u) != uf.find(v): # Check cycle condition
uf.union(u, v)
total_weight += w
mst_edges.append(edge)
# Ensure that we have exactly V-1 edges in our result
if len(mst_edges) == N - 1:
return total_weight
else:
return -1 # Not all nodes connected
if __name__ == "__main__":
sample_input = [
(1, 2, 12),
(1, 3, 9),
(1, 4, 11),
(1, 5, 3),
(2, 3, 6),
(2, 4, 9),
(3, 4, 4),
(4, 5, 6)
]
num_nodes = 5
cost = kruskal(sample_input, num_nodes)
print("Total minimum cost:", cost)
```
---
### 方法二:Prim 算法
#### 思想描述
Prim 算法同样利用了贪心思想,不过它采取的是逐步扩展已有部分的方式构建最小生成树:
- 任选一个起点作为初始子图的一部分;
- 不断从未被纳入的部分挑选与当前子图相连且具有最小权重的边加入子图;
- 循环执行直至覆盖所有顶点[^4]。
#### 时间复杂度
使用邻接矩阵表示时,时间复杂度为 $O(V^2)$;而借助堆优化后的版本可达到 $O((V+E)\log V)$[^4]。
#### Python 实现
```python
import heapq
def prim(graph, start_vertex):
"""
使用 Prim 算法计算最小生成树
:param graph: 邻接表形式表示的加权无向图 {node: [(neighbor,weight)]}
:param start_vertex: 起始节点
:return: 最小生成树的总权重
"""
pq = [] # Priority queue of tuples (cost,node)
visited = set()
total_cost = 0
# Initialize with starting vertex and its outgoing connections
for neighbor, cost in graph[start_vertex]:
heapq.heappush(pq, (cost, neighbor))
while pq:
current_cost, next_node = heapq.heappop(pq)
if next_node not in visited:
visited.add(next_node)
total_cost += current_cost
for new_neighbor, new_cost in graph[next_node]:
if new_neighbor not in visited:
heapq.heappush(pq, (new_cost, new_neighbor))
if len(visited) == len(graph):
return total_cost
else:
return -1 # Graph is disconnected
if __name__ == "__main__":
adj_list_graph = {
1: [(2, 12), (3, 9), (4, 11), (5, 3)],
2: [(1, 12), (3, 6), (4, 9)],
3: [(1, 9), (2, 6), (4, 4)],
4: [(1, 11), (2, 9), (3, 4), (5, 6)],
5: [(1, 3), (4, 6)]
}
initial_vertex = 1
min_total_cost = prim(adj_list_graph, initial_vertex)
print("Total minimum cost using Prim's algorithm:", min_total_cost)
```
---
### 结果分析
无论是采用 Krusakl 还是 Prim 算法,最终都可以得到相同的最小生成树总权重。然而两者适用场景略有不同——当面对稀疏图时,由于 Krusakl 主要依赖于边的数量,因此表现更好;而对于稠密图来说,Prim 则更为高效[^4]。
---