最小生成树(Minimum Spanning Tree, MST) 是指在一个无向加权图
中,使得所有节点连通且总权重最小的一棵树。最小生成树具有以下特性:
- 树的性质:最小生成树包含图中的所有顶点,但没有环。
- 连通性:最小生成树确保图中的所有顶点都是连通的。
- 最小权重:最小生成树的边的总权重是所有可能的生成树中最小的。
求解最小生成树的常用算法
1. Kruskal算法:
- 核心思想是贪心策略,即每次选择权重最小的边,前提是不形成环。
- 具体步骤:
(1)将图中的所有边按权重从小到大排序。
(2)依次选择排序后的边,若加入该边不形成环,则将该边加入生成树。
(3)重复上述步骤,直到生成树包含图中所有顶点。
以下是Kruskal算法在C++中的实现。这个实现使用了并查集(Union-Find)
数据结构来检测和避免环的形成。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 边的数据结构
struct Edge {
int u, v, weight;
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
// 并查集数据结构
class UnionFind {
public:
UnionFind(int n) : parent(n), rank(n, 0) {
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
}
int find(int u) {
if (u != parent[u]) {
parent[u] = find(parent[u]); // 路径压缩
}
return parent[u];
}
bool unite(int u, int v) {
int rootU = find(u);
int rootV = find(v);
if (rootU != rootV) {
if (rank[rootU] < rank[rootV]) {
swap(rootU, rootV);
}
parent[rootV] = rootU;
if (rank[rootU] == rank[rootV]) {
++rank[rootU];
}
return true;
}
return false;
}
private:
vector<int> parent;
vector<int> rank;
};
// Kruskal算法实现
vector<Edge> kruskal(int n, vector<Edge>& edges) {
sort(edges.begin(), edges.end());
UnionFind uf(n);
vector<Edge> mst;
for (const Edge& edge : edges) {
if (uf.unite(edge.u, edge.v)) {
mst.push_back(edge);
}
}
return mst;
}
int main() {
int n = 4; // 图中顶点的数量
vector<Edge> edges = {
{0, 1, 1},
{0, 2, 2},
{0, 3, 3},
{1, 3, 1},
{2, 3, 4}
};
vector<Edge> mst = kruskal(n, edges);
cout << "Minimum Spanning Tree:" << endl;
for (const Edge& edge : mst) {
cout << edge.u << " - " << edge.v << " : " << edge.weight << endl;
}
return 0;
}
代码解释
-
Edge 结构体: 表示一条边,包括边的两个顶点 u u u 和 v v v,以及边的权重 w e i g h t weight weight。重载了小于运算符,以便按权重排序。
-
UnionFind 类: 并查集实现,用于管理顶点集合,并支持路径压缩和按秩合并。
find
方法:查找顶点的根,使用路径压缩优化。unite
方法:合并两个集合,使用按秩合并优化,避免形成环。
-
kruskal 函数: Kruskal算法的实现。
- 按权重对边进行排序。
- 使用并查集管理顶点集合,逐条检查边,若添加该边不形成环,则将其添加到生成树中。
-
main 函数: 设置图的顶点和边,调用 Kruskal 算法并输出最小生成树。
2. Prim算法:
- 核心思想也是贪心策略,但每次选择与生成树最近的顶点加入生成树。
- 具体步骤:
(1)从任意一个顶点开始,标记为已访问。
(2)选择一条连接已访问顶点和未访问顶点的权重最小的边,将对应的未访问顶点加入生成树。
(3)重复上述步骤,直到所有顶点都被访问。
以下是Prim算法在C++中的实现。这个实现使用了优先队列(堆)来选择权重最小的边,并逐步构建最小生成树。
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <utility>
#include <climits>
using namespace std;
// 定义邻接表中边的结构
typedef pair<int, int> Edge; // first: 权重, second: 目标顶点
// Prim算法实现
void prim(int n, vector<vector<Edge>>& graph) {
vector<int> parent(n, -1); // 最小生成树中每个顶点的父节点
vector<int> key(n, INT_MAX); // 每个顶点到最小生成树的最小权重
vector<bool> inMST(n, false); // 标记顶点是否已经在最小生成树中
priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
pq.push({0, 0}); // 从顶点0开始
key[0] = 0;
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (inMST[u]) continue;
inMST[u] = true;
for (const auto& [weight, v] : graph[u]) {
if (!inMST[v] && weight < key[v]) {
key[v] = weight;
pq.push({key[v], v});
parent[v] = u;
}
}
}
cout << "Minimum Spanning Tree:" << endl;
for (int i = 1; i < n; ++i) {
cout << parent[i] << " - " << i << endl;
}
}
int main() {
int n = 5; // 图中顶点的数量
vector<vector<Edge>> graph(n);
// 添加边(无向图)
graph[0].emplace_back(2, 1);
graph[1].emplace_back(2, 0);
graph[0].emplace_back(3, 3);
graph[3].emplace_back(3, 0);
graph[1].emplace_back(3, 3);
graph[3].emplace_back(3, 1);
graph[1].emplace_back(8, 4);
graph[4].emplace_back(8, 1);
graph[1].emplace_back(6, 2);
graph[2].emplace_back(6, 1);
graph[2].emplace_back(7, 4);
graph[4].emplace_back(7, 2);
graph[3].emplace_back(5, 4);
graph[4].emplace_back(5, 3);
prim(n, graph);
return 0;
}
代码解释
-
Edge 类型定义 :
typedef pair<int, int> Edge
,其中first
表示权重,second
表示目标顶点。 -
prim 函数: 实现Prim算法,使用优先队列(最小堆)来选择最小权重的边。
parent
:存储每个顶点在最小生成树中的父节点。key
:存储每个顶点到最小生成树的最小权重。inMST
:标记每个顶点是否已经包含在最小生成树中。priority_queue
:优先队列,按照边的权重从小到大排序,使用greater<Edge>
使其成为最小堆。- 从顶点0开始,将顶点0加入优先队列,并设置其权重为0。
- 逐步从优先队列中取出权重最小的边,并将对应的顶点加入最小生成树。
- 更新连接该顶点的所有未包含在最小生成树中的顶点的最小权重。
-
main 函数:设置图的顶点和边,调用Prim算法并输出最小生成树。
- 构建图的邻接表表示,添加无向边。
这个实现适用于任意无向加权图,并高效地找到最小生成树。