C++ 解决一个简单的图论问题 —— 最小生成树(以 Prim 算法为例)

使用 C++ 解决一个简单的图论问题 ------ 最小生成树(以 Prim 算法为例),并且使用 Graphviz 库来生成结果图。

在图论中,"边权之和最小" 是最小生成树(MST)的核心目标,其含义和背景可以从以下几个方面解释:

一、基础定义:什么是 "边权之和"?

  • 边权:图中每条边的权重(Weight),可以代表实际问题中的成本、距离、时间、容量等量化指标。
  • 边权之和:对于一个子图(如生成树),将其中所有边的权重相加得到的总和。

二、"最小" 的具体含义:为什么要让边权之和最小?

无向连通图 中,生成树的定义是:

  • 包含图中 所有顶点(必须覆盖所有节点)。
  • 是一棵 (即无环,且边数为 \(|V| - 1\),V 为顶点数)。

最小生成树(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)\) 需满足:

  1. \(E_T \subseteq E\),且 \(|E_T| = |V| - 1\)(无环,连通所有顶点)。
  2. 目标是最小化 \(\sum_{e \in E_T} w(e)\),即:\(\min \sum_{e \in E_T} w(e)\)

四、实际意义:为什么需要 "边权之和最小"?

"边权之和最小" 的应用场景通常与 优化问题 相关,例如:

  1. 网络建设
    • 若边权代表铺设电缆的成本,最小生成树表示用最小成本连接所有节点的方案。
  2. 电路设计
    • 边权代表导线长度,MST 可最小化电路中导线的总长度。
  3. 物流规划
    • 边权代表运输距离,MST 可找到连接所有地点的最短路径网络(无环,避免冗余)。

五、与 "生成树" 的本质区别

  • 生成树:只要满足 "包含所有顶点且无环" 即可,不考虑边权和。
  • 最小生成树 :在所有生成树中,额外要求边权和 全局最小,是一种带优化目标的生成树。 (注:若图不连通,则不存在生成树;若边权可能为负,Prim 算法仍适用,但 Kruskal 算法需调整)

六、如何求解 "边权之和最小"?

通过 贪心算法 (如 Prim 算法、Kruskal 算法)或 优先队列优化 来高效找到 MST,核心思想是:

  • Prim 算法:从一个顶点出发,每次选择当前生成树到未访问顶点的最小边("加点法")。
  • Kruskal 算法:按边权从小到大排序,每次选择不构成环的最小边("加边法")。

总结

"边权之和最小" 是最小生成树的核心目标,它要求在连通所有顶点的无环子图中,选择边权总和最小的方案。这一概念在实际问题中对应 "最小成本连接所有节点",通过经典算法可高效求解,是图论中优化问题的基础应用之一。

实现思路

  1. Prim 算法:用于在加权连通图中找到最小生成树。
  2. Graphviz:一个开源的图形可视化工具,通过生成 DOT 语言描述的图形文件,然后转换为图片格式(如 PNG)。

代码解释

  1. Prim 算法:通过优先队列来选择当前最小权值的边,逐步构建最小生成树。
  2. 生成 DOT 文件:将图的信息和最小生成树的边信息写入 DOT 文件,方便后续使用 Graphviz 工具将其转换为图片。

使用方法

  1. 编译并运行上述代码,会生成一个名为 mst.dot 的文件。

  2. 安装 Graphviz 工具,然后在命令行中运行以下命令将 DOT 文件转换为 PNG 图片:

    dot -Tpng mst.dot -o mst.png

这样就可以得到一个可视化的最小生成树图片。

Prim 算法与最小生成树问题

1. 什么是 Prim 算法?

Prim 算法是一种用于求解 ** 加权无向图中最小生成树(Minimum Spanning Tree, MST)** 的贪心算法。 最小生成树 的定义:包含图中所有顶点,且边权之和最小的无环子图(树结构)。 Prim 算法的核心思想是:从任意一个顶点出发,逐步扩展生成树,每次选择当前生成树到其他顶点的最小边,直到所有顶点都被包含

2. Prim 算法的核心步骤(以邻接表 + 优先队列实现为例)

假设图为 \(G = (V, E)\),顶点集合 \(V = \{0, 1, 2, \dots, n-1\}\),边权非负。

步骤 1:初始化
  • 选择一个起始顶点(如顶点 0),标记为 "已访问",加入生成树。
  • 维护三个数组:
    • key[]:记录每个顶点到当前生成树的最小边权,初始化为无穷大(起始顶点的key设为 0)。
    • parent[]:记录生成树中每个顶点的父节点,用于重构生成树。
    • visited[]:标记顶点是否已加入生成树。
步骤 2:扩展生成树(贪心选择)
  • 使用 ** 优先队列(最小堆)** 存储顶点,按key值排序,每次取出key最小的顶点 u。
  • 遍历 u 的所有邻接顶点 v:
    • 如果 v 未被访问,且 u 到 v 的边权小于 v 的当前key值:
      • 更新 v 的key值为该边权。
      • 记录 v 的父节点为 u。
      • 将 v 加入优先队列。
步骤 3:重复直到所有顶点加入
  • 重复步骤 2,直到所有顶点被标记为 "已访问"。此时,parent[]数组存储了最小生成树的边关系。
  1. 初始化 :起始顶点 0,key[0]=0,其他顶点key为无穷大。
  2. 第一次迭代 :取出顶点 0,遍历邻接顶点 1(边权 2)、3(边权 6)。更新它们的key为 2 和 6,父节点为 0。
  3. 第二次迭代 :优先队列中最小key是顶点 1(key=2),取出后遍历其邻接顶点 0(已访问)、2(边权 3)、3(边权 8)、4(边权 5)。更新顶点 2 的key为 3,顶点 4 的key为 5,父节点分别为 1。
  4. 第三次迭代 :最小key是顶点 2(key=3),取出后遍历邻接顶点 1(已访问)、4(边权 7)。顶点 4 的当前key是 5,7 大于 5,不更新。
  5. 第四次迭代 :最小key是顶点 4(key=5),取出后遍历邻接顶点 1(已访问)、2(已访问)、3(边权 9)。顶点 3 的当前key是 6,9 大于 6,不更新。
  6. 第五次迭代:最后取出顶点 3(key=6),所有顶点已访问,算法结束。

最终生成树的边为: 0-1(2)、1-2(3)、1-4(5)、0-3(6),总权值 2+3+5+6=16。

4. 算法复杂度
  • 邻接矩阵 + 数组实现 (适合稠密图):时间复杂度 \(O(V^2)\),无需优先队列,每次遍历所有未访问顶点找最小key
  • 邻接表 + 优先队列实现(适合稀疏图):时间复杂度 \(O(E \log V)\),每次更新优先队列的时间为 \(O(\log V)\)。
5. 与 Kruskal 算法的区别
  • Prim 算法加点法 ,从顶点出发,每次扩展生成树的顶点,适合边稠密或顶点少的图。
  • Kruskal 算法加边法 ,按边权从小到大排序,每次选不构成环的最小边,适合边稀疏的图。
6. 应用场景
  • 网络设计(如最小成本连接所有节点)。
  • 电路设计(最小化导线长度)。
  • 聚类问题(构建最小生成树后按边权切割)。
总结

Prim 算法通过贪心策略,每次选择当前生成树到未访问顶点的最小边,逐步构建最小生成树。其核心是局部最优选择(最小边权)推导出全局最优解,适用于边权非负的无向图,是解决最小生成树问题的经典算法之一。

cpp 复制代码
#include <iostream>
#include <vector>
#include <queue>
#include <fstream>
#include <limits>

using namespace std;

// 定义边的结构体
struct Edge {
    int to;
    int weight;
    Edge(int t, int w) : to(t), weight(w) {}
};

// 定义图的邻接表表示
using Graph = vector<vector<Edge>>;

// Prim 算法求最小生成树
vector<Edge> prim(const Graph& graph) {
    int n = graph.size();
    vector<bool> visited(n, false);
    vector<Edge> mst;
    priority_queue<pair<int, pair<int, int>>, vector<pair<int, pair<int, int>>>, greater<pair<int, pair<int, int>>>> 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<Edge>& 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<Edge> mst = prim(graph);

    // 生成 DOT 文件
    generateDotFile(graph, mst, "mst.dot");

    cout << "最小生成树的边已计算,DOT 文件已生成。" << endl;

    return 0;
}
相关推荐
xin007hoyo11 分钟前
算法笔记.染色法判断二分图
数据结构·笔记·算法
এ᭄画画的北北2 小时前
力扣-234.回文链表
算法·leetcode·链表
八股文领域大手子3 小时前
深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现
java·数据库·算法·缓存·mybatis·哈希算法
wuqingshun3141595 小时前
蓝桥杯 11. 打印大X
数据结构·算法·职场和发展·蓝桥杯·深度优先
Blossom.1185 小时前
量子网络:构建未来通信的超高速“高速公路”
网络·opencv·算法·安全·机器学习·密码学·量子计算
A林玖5 小时前
【机器学习】朴素贝叶斯
人工智能·算法·机器学习
六边形战士DONK6 小时前
神经网络基础[损失函数,bp算法,梯度下降算法 ]
人工智能·神经网络·算法
wuqingshun3141596 小时前
蓝桥杯 2. 确定字符串是否是另一个的排列
数据结构·c++·算法·职场和发展·蓝桥杯
小刘|6 小时前
JVM 自动内存管理
java·jvm·算法