使用 C++ 解决一个简单的图论问题 —— 最小生成树(以 Prim 算法为例),并且使用 Graphviz 库来生成结果图。
在图论中,“边权之和最小” 是最小生成树(MST)的核心目标,其含义和背景可以从以下几个方面解释:
在 无向连通图 中,生成树的定义是:
最小生成树(MST) 是所有可能的生成树中,边权之和最小的那一棵。 例如,假设有一个包含 4 个顶点的图,可能有多种生成树(如下图示),其中边权和最小的即为 MST:
顶点A ─(2)─ 顶点B 顶点A ─(1)─ 顶点C
│(3) │(1)
顶点C ─(1)─ 顶点D 顶点B ─(3)─ 顶点D
边权和:2+3+1=6 边权和:1+1+3=5(MST)
设图 \(G = (V, E)\),其中顶点集合 \(V = \{v_1, v_2, \dots, v_n\}\),边集合 \(E = \{e_1, e_2, \dots, e_m\}\),每条边 \(e_i\) 的权重为 \(w(e_i)\)。 生成树 \(T = (V, E_T)\) 需满足:
“边权之和最小” 的应用场景通常与 优化问题 相关,例如:
通过 贪心算法(如 Prim 算法、Kruskal 算法)或 优先队列优化 来高效找到 MST,核心思想是:
“边权之和最小” 是最小生成树的核心目标,它要求在连通所有顶点的无环子图中,选择边权总和最小的方案。这一概念在实际问题中对应 “最小成本连接所有节点”,通过经典算法可高效求解,是图论中优化问题的基础应用之一。
mst.dot
的文件。dot -Tpng mst.dot -o mst.png
这样就可以得到一个可视化的最小生成树图片。
Prim 算法是一种用于求解 ** 加权无向图中最小生成树(Minimum Spanning Tree, MST)** 的贪心算法。 最小生成树的定义:包含图中所有顶点,且边权之和最小的无环子图(树结构)。 Prim 算法的核心思想是:从任意一个顶点出发,逐步扩展生成树,每次选择当前生成树到其他顶点的最小边,直到所有顶点都被包含。
假设图为 \(G = (V, E)\),顶点集合 \(V = \{0, 1, 2, \dots, n-1\}\),边权非负。
key[]
:记录每个顶点到当前生成树的最小边权,初始化为无穷大(起始顶点的key
设为 0)。parent[]
:记录生成树中每个顶点的父节点,用于重构生成树。visited[]
:标记顶点是否已加入生成树。key
值排序,每次取出key
最小的顶点 u。key
值:
key
值为该边权。parent[]
数组存储了最小生成树的边关系。key[0]=0
,其他顶点key
为无穷大。key
为 2 和 6,父节点为 0。key
是顶点 1(key=2),取出后遍历其邻接顶点 0(已访问)、2(边权 3)、3(边权 8)、4(边权 5)。更新顶点 2 的key
为 3,顶点 4 的key
为 5,父节点分别为 1。key
是顶点 2(key=3),取出后遍历邻接顶点 1(已访问)、4(边权 7)。顶点 4 的当前key
是 5,7 大于 5,不更新。key
是顶点 4(key=5),取出后遍历邻接顶点 1(已访问)、2(已访问)、3(边权 9)。顶点 3 的当前key
是 6,9 大于 6,不更新。最终生成树的边为: 0-1(2)、1-2(3)、1-4(5)、0-3(6),总权值 2+3+5+6=16。
key
。Prim 算法通过贪心策略,每次选择当前生成树到未访问顶点的最小边,逐步构建最小生成树。其核心是局部最优选择(最小边权)推导出全局最优解,适用于边权非负的无向图,是解决最小生成树问题的经典算法之一。
#include
#include
#include
#include
#include
using namespace std;
// 定义边的结构体
struct Edge {
int to;
int weight;
Edge(int t, int w) : to(t), weight(w) {}
};
// 定义图的邻接表表示
using Graph = vector>;
// Prim 算法求最小生成树
vector prim(const Graph& graph) {
int n = graph.size();
vector visited(n, false);
vector mst;
priority_queue>, vector>>, greater>>> pq;
// 从顶点 0 开始
visited[0] = true;
for (const Edge& edge : graph[0]) {
pq.push({ edge.weight, {0, edge.to} });
}
while (!pq.empty()) {
auto [weight, nodes] = pq.top();
pq.pop();
int u = nodes.first;
int v = nodes.second;
if (visited[v]) continue;
visited[v] = true;
mst.emplace_back(v, weight);
for (const Edge& edge : graph[v]) {
if (!visited[edge.to]) {
pq.push({ edge.weight, {v, edge.to} });
}
}
}
return mst;
}
// 生成 DOT 文件
void generateDotFile(const Graph& graph, const vector& mst, const string& filename) {
ofstream dotFile(filename);
if (!dotFile.is_open()) {
cerr << "无法打开文件: " << filename << endl;
return;
}
dotFile << "graph G {" << endl;
// 绘制所有边
for (int u = 0; u < graph.size(); ++u) {
for (const Edge& edge : graph[u]) {
if (u < edge.to) {
dotFile << " " << u << " -- " << edge.to << " [label=\"" << edge.weight << "\"];" << endl;
}
}
}
// 突出显示最小生成树的边
for (const Edge& edge : mst) {
int u = -1; // 这里需要根据 mst 找到对应的 u,假设第一个节点是 0
for (int i = 0; i < graph.size(); ++i) {
for (const Edge& e : graph[i]) {
if (e.to == edge.to && e.weight == edge.weight) {
u = i;
break;
}
}
if (u != -1) break;
}
dotFile << " " << u << " -- " << edge.to << " [color=red, penwidth=3];" << endl;
}
dotFile << "}" << endl;
dotFile.close();
}
int main() {
// 示例图
Graph graph = {
{{1, 2}, {3, 6}},
{{0, 2}, {2, 3}, {3, 8}, {4, 5}},
{{1, 3}, {4, 7}},
{{0, 6}, {1, 8}, {4, 9}},
{{1, 5}, {2, 7}, {3, 9}}
};
// 计算最小生成树
vector mst = prim(graph);
// 生成 DOT 文件
generateDotFile(graph, mst, "mst.dot");
cout << "最小生成树的边已计算,DOT 文件已生成。" << endl;
return 0;
}