深入浅出【最小生成树】:Prim与Kruskal算法详解
深入浅出最小生成树:Prim与Kruskal算法详解
什么是最小生成树 (Minimum Spanning Tree, MST)?
在图论中,我们经常会遇到这样一个经典问题:对于一个带权的无向连通图,如何找到一个无环的子集,使得这个子集连接了图中所有的顶点,并且所有边的权重之和最小。
这个子集就构成了一棵最小生成树(MST)。它有两个关键属性:
- 是一棵树:意味着无环且连通。
- 权重最小:在所有可能的生成树中,它的总边权最小。
应用场景:最小生成树在现实生活中应用广泛,例如:
- 网络布线:用最少的线缆连接所有机房或城市。
- 交通规划:建设成本最低的公路网连接所有城镇。
- 电路设计:用最少的线路连接多个元件。
两大经典算法
解决MST问题最著名的两个算法是Prim算法和Kruskal算法。它们都基于贪心算法的思想,即在每一步选择中都采取当前状态下最优的选择。
1. Prim算法(普里姆算法)
原理解释
Prim算法的核心思想是:从某一个顶点开始,逐步“长大”一棵树。每次将一条连接树与非树节点的最小权重边及其连接的顶点加入到树中。
你可以把它想象成“种树”的过程:
- 随便撒下一颗种子(选择一个起始顶点)。
- 找到离这棵树最近的一棵“树苗”(权重最小的边连接的顶点)。
- 把这棵“树苗”和连接它的那根“树枝”(边)并入大树。
- 重复步骤2和3,直到所有的“树苗”都被并入大树。
算法步骤
- 初始化:任选一个顶点作为起始点,加入集合
S
(代表已在MST中的顶点)。初始化一个数组dist
(或key
),记录所有不在S
中的顶点到S
的最短距离(即与S
中任意顶点相连的最小边权)。初始时,起始点的dist
设为0,其他点为无穷大(INF
)。 - 循环迭代:重复以下步骤
V-1
次(V
为顶点数):
a. 贪心选择:从不在S
的顶点中,选出dist
值最小的顶点u
。
b. 加入集合:将u
加入集合S
。此时,连接u
和S
的那条边(就是使得dist[u]
最小的边)就是MST的一条边。
c. 更新信息:检查所有与u
相邻且不在S
中的顶点v
。如果边(u, v)
的权重小于v
当前的dist[v]
值,则更新dist[v] = graph[u][v]
(同时可以记录parent[v] = u
,表示v
是通过u
加入树的)。 - 结束:最终,
parent
数组和dist
数组共同描述了整棵最小生成树。
代码实现
C++实现(使用邻接矩阵)
#include <iostream>
#include <vector>
#include <climits>
using namespace std;#define V 5 // 图中顶点的数量int minKey(const vector<int>& key, const vector<bool>& inMST) {int min = INT_MAX, min_index;for (int v = 0; v < V; v++) {if (!inMST[v] && key[v] < min) {min = key[v];min_index = v;}}return min_index;
}void printMST(const vector<int>& parent, const vector<vector<int>>& graph) {cout << "Edge \tWeight\n";for (int i = 1; i < V; i++) {cout << parent[i] << " - " << i << " \t" << graph[i][parent[i]] << " \n";}
}void primMST(const vector<vector<int>>& graph) {vector<int> parent(V); // 存储MST的结构vector<int> key(V, INT_MAX); // 记录顶点到MST的最小权值vector<bool> inMST(V, false); // 记录顶点是否已在MST中key[0] = 0; // 选择第0个顶点作为起点parent[0] = -1; // 第一个节点是树的根,没有父节点for (int count = 0; count < V - 1; count++) {int u = minKey(key, inMST); // 选取key最小的顶点inMST[u] = true; // 将其加入MST// 更新所有与u相邻的顶点的key值和parentfor (int v = 0; v < V; v++) {if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {parent[v] = u;key[v] = graph[u][v];}}}printMST(parent, graph);
}int main() {vector<vector<int>> 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 } };primMST(graph);return 0;
}
Python实现
import sysclass Graph():def __init__(self, vertices):self.V = verticesself.graph = [[0 for _ in range(vertices)] for _ in range(vertices)]def print_mst(self, parent):print("Edge \tWeight")for i in range(1, self.V):print(f"{parent[i]} - {i} \t{self.graph[i][parent[i]]}")def min_key(self, key, mst_set):min_val = sys.maxsizemin_index = -1for v in range(self.V):if key[v] < min_val and not mst_set[v]:min_val = key[v]min_index = vreturn min_indexdef prim_mst(self):key = [sys.maxsize] * self.V # 初始化key值为无穷大parent = [None] * self.V # 存储MSTkey[0] = 0 # 选择第一个顶点作为起点mst_set = [False] * self.V # 记录顶点是否在MST中parent[0] = -1 # 根节点没有父节点for _ in range(self.V - 1):u = self.min_key(key, mst_set)mst_set[u] = Truefor v in range(self.V):if self.graph[u][v] > 0 and not mst_set[v] and key[v] > self.graph[u][v]:key[v] = self.graph[u][v]parent[v] = uself.print_mst(parent)if __name__ == '__main__':g = Graph(5)g.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]]g.prim_mst()
2. Kruskal算法(克鲁斯卡尔算法)
原理解释
Kruskal算法的核心思想是:按权重从小到大选择边,如果这条边不会与已选择的边构成环,就将其加入MST。
你可以把它想象成“拼图”的过程:
- 把所有边按权重从小到大排序。
- 初始化一个只有
V
个顶点,没有边的森林。 - 从权重最小的边开始尝试:
- 如果这条边连接的两个顶点不在同一个连通分量中(即加入后不会形成环),就将这条边加入MST,并合并这两个连通分量。
- 否则,丢弃这条边。
- 重复步骤3,直到MST中有
V-1
条边。
这里的关键是如何高效判断是否成环,这可以通过并查集 (Union-Find) 数据结构来高效实现。
算法步骤
- 排序:将图的所有边按权重从小到大排序。
- 初始化:初始化一个并查集,每个顶点自成一个集合。初始化MST为空。
- 处理边:按顺序遍历每一条边:
a. 检查环:使用并查集检查这条边连接的两个顶点u
和v
是否在同一个集合中。
b. 若无环则加入:如果不在,说明加入这条边不会形成环,将其加入MST。然后在并查集中合并u
和v
所在的集合。 - 结束:当MST中有
V-1
条边时,算法结束。
代码实现
C++实现(使用并查集)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;class Edge {
public:int src, dest, weight;// 用于sort函数比较bool operator<(const Edge& other) const {return weight < other.weight;}
};class Subset {
public:int parent;int rank;
};class Graph {
public:int V, E;vector<Edge> edges;Graph(int v, int e) : V(v), E(e) {}int find(Subset subsets[], int i) {if (subsets[i].parent != i)subsets[i].parent = find(subsets, subsets[i].parent);return subsets[i].parent;}void Union(Subset subsets[], int x, int y) {int xroot = find(subsets, x);int yroot = find(subsets, y);if (subsets[xroot].rank < subsets[yroot].rank)subsets[xroot].parent = yroot;else if (subsets[xroot].rank > subsets[yroot].rank)subsets[yroot].parent = xroot;else {subsets[yroot].parent = xroot;subsets[xroot].rank++;}}void kruskalMST() {vector<Edge> result; // 存储MST的结果int e = 0; // 用于遍历排序后的边int i = 0; // 用于result的索引// 1. 排序所有边sort(edges.begin(), edges.end());// 分配内存并创建V个子集Subset* subsets = new Subset[V];for (int v = 0; v < V; v++) {subsets[v].parent = v;subsets[v].rank = 0;}// MST的边数应为 V-1while (i < V - 1 && e < E) {// 2. 选取最小的边Edge next_edge = edges[e++];int x = find(subsets, next_edge.src);int y = find(subsets, next_edge.dest);// 如果不在同一集合,加入不会成环if (x != y) {result.push_back(next_edge);Union(subsets, x, y);i++;}}// 输出MSTcout << "Edge \tWeight\n";for (i = 0; i < result.size(); i++) {cout << result[i].src << " - " << result[i].dest << " \t" << result[i].weight << endl;}delete[] subsets;}
};int main() {int V = 4;int E = 5;Graph graph(V, E);graph.edges = {{0, 1, 10},{0, 2, 6},{0, 3, 5},{1, 3, 15},{2, 3, 4}};graph.kruskalMST();return 0;
}
Python实现
class Graph:def __init__(self, vertices):self.V = verticesself.graph = [] # 存储所有边的列表def add_edge(self, u, v, w):self.graph.append([u, v, w])def find(self, parent, i):# 查找根节点if parent[i] != i:parent[i] = self.find(parent, parent[i]) # 路径压缩return parent[i]def union(self, parent, rank, x, y):# 按秩合并xroot = self.find(parent, x)yroot = self.find(parent, y)if rank[xroot] < rank[yroot]:parent[xroot] = yrootelif rank[xroot] > rank[yroot]:parent[yroot] = xrootelse:parent[yroot] = xrootrank[xroot] += 1def kruskal_mst(self):result = [] # 存储MST的边i = 0 # 遍历边的索引e = 0 # 结果中边的计数# 1. 按权重排序所有边self.graph = sorted(self.graph, key=lambda item: item[2])parent = []rank = []# 2. 初始化并查集for node in range(self.V):parent.append(node)rank.append(0)# 需要添加 V-1 条边while e < self.V - 1:if i >= len(self.graph):breaku, v, w = self.graph[i]i += 1x = self.find(parent, u)y = self.find(parent, v)# 如果不在同一集合,加入结果并合并if x != y:e += 1result.append([u, v, w])self.union(parent, rank, x, y)# 输出结果print("Edge \tWeight")for u, v, w in result:print(f"{u} -- {v} \t{w}")if __name__ == '__main__':g = Graph(4)g.add_edge(0, 1, 10)g.add_edge(0, 2, 6)g.add_edge(0, 3, 5)g.add_edge(1, 3, 15)g.add_edge(2, 3, 4)g.kruskal_mst()
算法对比与总结
特性 | Prim算法 | Kruskal算法 |
---|---|---|
核心思想 | 从一个点开始,逐步扩张树 | 按权重排序边,逐个添加不构成环的边 |
数据结构 | 优先队列(堆)、key 数组 | 并查集、排序后的边集合 |
时间复杂度 | O(V^2) (邻接矩阵)O(E log V) (邻接表+二叉堆) | O(E log E) 或 O(E log V) (主要来自排序) |
适用场景 | 稠密图(边多顶点少) | 稀疏图(边少顶点多) |
贪心策略 | 顶点贪心 | 边贪心 |
如何选择?
- 如果图的边数量非常庞大(稠密图),接近完全图,使用Prim算法(尤其是简单实现)可能更高效。
- 如果图的边数量相对较少(稀疏图),Kruskal算法因其简单的排序和并查集操作,通常更容易实现且效率更高。
希望这篇博客能帮助你彻底理解最小生成树和这两大经典算法!