算法思想
最小生成树(MST)问题的核心思想基于一个贪心策略:我们总是选择当前可用的最小权值边,同时保证不形成环路。这个简单而强大的思想支撑了两个经典算法。
Kruskal 算法
基本思路
- 将所有边按权重从小到大排序
- 依次考察每条边,如果这条边连接的两个顶点不在同一个连通分量中,则选择这条边加入最小生成树
- 重复步骤2,直到选择了n-1条边(或所有顶点都在同一连通分量中)
核心思想解析
Kruskal算法的贪心策略是"全局贪心"---考虑整个图中权值最小的边。算法维护一个森林,初始时每个顶点自成一棵树。随着边的加入,森林中的树逐渐合并,最终形成一棵包含所有顶点的最小生成树。
关键点
- 使用并查集(Union-Find)来高效判断两个顶点是否在同一连通分量
- 对边排序是算法效率的主要瓶颈
- 特别适合稀疏图(边数远小于顶点数的平方)
Prim 算法
基本思路
- 从任意顶点开始,将其加入最小生成树
- 在所有连接"已选顶点集合"和"未选顶点集合"的边中,选择权值最小的边
- 将这条边以及对应的未选顶点加入最小生成树
- 重复步骤2和3,直到所有顶点都加入最小生成树
核心思想解析
Prim算法的贪心策略是"局部贪心"---考虑当前树到其余顶点的最小边。算法维护一个不断生长的树,每次选择一个最近的顶点加入。
关键点
- 使用优先队列维护未加入顶点到当前树的最小距离
- 每次选择距离当前树最近的顶点
- 特别适合稠密图(边数接近顶点数的平方)
算法比较
- Kruskal:O(E log E),适合稀疏图
- Prim:O(E log V),适合稠密图
- 两种算法在正确实现的情况下都能得到最小生成树(如有多个,可能不同)
代码实现
Kruskal 算法实现
cpp
struct Edge {
int u, v, weight;
bool operator<(const Edge &other) const {
return weight < other.weight;
}
};
class UnionFind {
private:
vector<int> parent, rank;
public:
UnionFind(int n) {
parent.resize(n + 1);
rank.resize(n + 1, 0);
for (int i = 1; i <= n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return false;
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
if (rank[rootX] == rank[rootY]) rank[rootX]++;
}
return true;
}
};
pair<int, vector<Edge>> kruskal(int n, vector<Edge> &edges) {
// 按权重排序所有边
sort(edges.begin(), edges.end());
UnionFind uf(n);
int total_weight = 0;
vector<Edge> mst;
for (const Edge &e : edges) {
if (uf.unite(e.u, e.v)) {
total_weight += e.weight;
mst.push_back(e);
if (mst.size() == n - 1) break; // 已经有n-1条边,MST构建完成
}
}
return {total_weight, mst};
}
Prim 算法实现
cpp
typedef pair<int, int> pii; // (weight, vertex)
pair<int, vector<pair<int, int>>> prim(vector<vector<pii>> &graph, int n) {
vector<bool> visited(n + 1, false);
priority_queue<pii, vector<pii>, greater<pii>> pq;
int total_weight = 0;
vector<pair<int, int>> mst_edges; // (u, v) 表示边
// 从顶点1开始
pq.push({0, 1}); // {weight, vertex}
vector<int> parent(n + 1, -1); // 用于记录MST中每个点的父节点
while (!pq.empty()) {
auto [weight, u] = pq.top();
pq.pop();
if (visited[u]) continue;
visited[u] = true;
// 将边加入MST(除了起始点)
if (parent[u] != -1) {
total_weight += weight;
mst_edges.push_back({parent[u], u});
}
// 检查所有相邻边
for (auto &[v, w] : graph[u]) {
if (!visited[v]) {
pq.push({w, v});
parent[v] = u;
}
}
}
return {total_weight, mst_edges};
}