攻克图论核心领域!详解最小生成树两大经典算法——Prim与Kruskal,从原理剖析到代码实战,彻底掌握连通图的最优解生成方法。
一、最小生成树(MST)核心概念
最小生成树是连通加权无向图中边权和最小的生成树,具有以下性质:
包含所有顶点且无环
任意两顶点间有且仅有一条路径
边数 = 顶点数 - 1
应用场景:
-
城市间光纤网络铺设
-
电路板布线优化
-
物流中心选址规划
二、Prim算法详解
算法思想(贪心策略)
从任意顶点出发,逐步扩展生成树,每次选择连接树与非树节点的最小权边对应的顶点加入生成树。
代码实现(邻接表 + 优先队列优化)
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> pii; // <权重, 目标顶点>
int primMST(vector<vector<pii>>& adj) {
int V = adj.size();
vector<bool> inMST(V, false);
priority_queue<pii, vector<pii>, greater<pii>> pq;
int totalWeight = 0;
pq.push({0, 0}); // 从顶点0开始
while (!pq.empty()) {
auto [weight, u] = pq.top();
pq.pop();
if (inMST[u]) continue;
inMST[u] = true;
totalWeight += weight;
for (auto& edge : adj[u]) {
int v = edge.second, w = edge.first;
if (!inMST[v]) {
pq.push({w, v});
}
}
}
return totalWeight;
}
// 测试用例
int main() {
vector<vector<pii>> graph = {
{{4,1}, {8,2}}, // 顶点0
{{4,0}, {8,2}, {11,3}},// 顶点1
{{8,0}, {2,3}, {7,4}}, // 顶点2
{{11,1}, {2,2}, {6,5}},// 顶点3
{{7,2}, {9,5}}, // 顶点4
{{6,3}, {9,4}} // 顶点5
};
cout << "最小生成树总权重:" << primMST(graph);
return 0;
}
三、Kruskal算法详解
算法思想(并查集应用)
按边权升序选择边,若该边连接的两个顶点不在同一连通分量,则加入生成树,避免形成环。
代码实现(并查集优化)
#include <algorithm>
struct Edge {
int src, dest, weight;
bool operator<(Edge const& other) {
return weight < other.weight;
}
};
class UnionFind {
vector<int> parent;
public:
UnionFind(int n) : parent(n) {
for(int i=0; i<n; ++i) parent[i] = i;
}
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
void unite(int x, int y) {
parent[find(x)] = find(y);
}
};
int kruskalMST(vector<Edge>& edges, int V) {
sort(edges.begin(), edges.end());
UnionFind uf(V);
int totalWeight = 0, edgeCount = 0;
for (Edge& e : edges) {
if (uf.find(e.src) != uf.find(e.dest)) {
totalWeight += e.weight;
uf.unite(e.src, e.dest);
if (++edgeCount == V-1) break;
}
}
return totalWeight;
}
// 测试用例
int main() {
vector<Edge> edges = {
{0,1,4}, {0,2,8}, {1,2,8}, {1,3,11},
{2,3,2}, {2,4,7}, {3,4,9}, {3,5,6}, {4,5,9}
};
cout << "最小生成树总权重:" << kruskalMST(edges, 6);
return 0;
}
四、算法对比与选型指南
特性 | Prim算法 | Kruskal算法 |
---|---|---|
时间复杂度 | O(E + V log V) 堆优化 | O(E log E) 排序耗时 |
空间复杂度 | O(V + E) | O(E) |
适用图类型 | 稠密图(邻接矩阵) | 稀疏图(边列表) |
数据结构 | 优先队列 | 并查集 + 排序 |
选择策略 | 顶点扩展 | 边选择 |
是否需要连通图 | 必须连通 | 可处理非连通图(生成森林) |
五、大厂真题实战
真题1:最低成本连通城市(某大厂2024笔试)
题目描述:
给定N个城市之间的道路成本,求使所有城市连通的最低成本(若无法连通返回-1)
解题思路:
-
Kruskal算法天然适合处理边列表形式输入
-
最终检查生成树边数是否为N-1
int minimumCost(int N, vector<vector<int>>& connections) {
sort(connections.begin(), connections.end(),
[](auto& a, auto& b){return a[2] < b[2];});
UnionFind uf(N+1); // 城市编号从1开始
int cost = 0, count = 0;
for(auto& conn : connections) {
if(uf.find(conn[0]) != uf.find(conn[1])) {
uf.unite(conn[0], conn[1]);
cost += conn[2];
if(++count == N-1) break;
}
}
return count == N-1 ? cost : -1;
}
真题2:关键连接边判断(某大厂2023面试)
问题描述:
找出图中所有存在于所有最小生成树中的边
解题思路:
-
计算原图MST的总权重W
-
对每条边e,检查:
-
e必须在某些MST中(权重 ≤ 所在环的最大边)
-
删除e后MST权重 > W 或无法形成生成树
-
六、常见误区与注意事项
-
负权边处理:两种算法均可处理负权边(MST允许负权)
-
图连通性检查:Prim需手动处理,Kruskal自动检测
-
边权相等处理:可能存在多个MST(需题目明确要求)
-
顶点编号:注意是否从0或1开始计数
-
大规模数据优化:
-
Prim使用斐波那契堆可优化至O(E + V log V)
-
Kruskal使用计数排序(当边权范围较小时)
-
七、总结与扩展
核心要点:
-
Prim适合顶点操作,Kruskal适合边操作
-
并查集的路径压缩优化是关键性能保障
-
两种算法在不同场景下的效率反转点(E ≈ V²时Prim更优)
扩展思考:
-
如何动态维护一个随时增减边的最小生成树?
-
当图中存在相同权重的边时,如何统计MST的数量?
-
如何利用MST解决旅行商问题(TSP)的近似解?
LeetCode真题练习: