当前位置:首页 > C++目录 > 正文内容

最小生成树—Prim(普里姆)算法

一、算法概述

Prim 算法是一种用于求解加权无向连通图的最小生成树(MST) 的贪心算法。它从一个顶点开始,逐步扩展生成树,每次选择连接已选顶点集和未选顶点集的最小权重边。

二、算法思想

  1. 初始化:从任意顶点开始,将其加入生成树集合

  2. 重复执行

    • 在所有连接已选顶点和未选顶点的边中,选择权重最小的边

    • 将该边加入生成树,并将对应的未选顶点加入已选集合

  3. 终止条件:当所有顶点都加入生成树时结束

三、算法过程示例

示例图(顶点:A,B,C,D;边权重如图):

更清晰的邻接关系(无向图,权重如图):

  • A–B : 2

  • A–C : 4

  • A–D : 3

  • B–D : 1

  • C–D : 5

顶点集合:{A, B, C, D}
目标:用 Prim 算法找最小生成树(MST)

每次选择连接 已选顶点集 与 未选顶点集 的最小权重边,将该边及其另一端点加入已选集合。

步骤 1

  • 已选集合 S={A}

  • 候选边(一端在 S,一端不在 S):

    • A–B (2)   (说明:A在S中,B不在S中)

    • A–C (4)   (说明:A在S中,C不在S中)

    • A–D (3)   (说明:A在S中,D不在S中)

  • 最小权重边:A–B (2)

  • 加入 B 到 S,边 A–B 加入 MST。

MST 边:{A–B}
S = {A, B}



步骤 2

  • S = {A, B}

  • 候选边(一端在 S,一端不在 S):

    • A–C (4) (说明:A在S中,C不在S中)

    • A–D (3) (说明:A在S中,D不在S中)

    • B–D (1)  (说明:B在S中,D不在S中)

  • 最小权重边:B–D (1)

  • 加入 D 到 S,边 B–D 加入 MST。

MST 边:{A–B, B–D}
S = {A, B, D}




步骤 3

  • S = {A, B, D}

  • 候选边(一端在 S,一端不在 S):

    • A–C (4)    (说明:A在S中,C不在S中)

    • A–D (3)   (说明:A在S中,D在S中,不能选,会形成环)

    • C–D (5)     (说出:C不在S中,D在S中)

  • 有效候选边(另一端不在 S):

    • A–C (4)

    • C–D (5)

  • 最小权重边:A–C (4)

  • 加入 C 到 S,边 A–C 加入 MST。

MST 边:{A–B, B–D, A–C}
S = {A, B, D, C}(已包含所有顶点)


步骤 4

所有顶点已在 S,算法结束。

MST 总权重=2+4+1=7


四、为什么是对的


Prim 算法的正确性基于 MST 的割性质(Cut Property):

对于图的任意一个割(把顶点分成两个集合 S 和 V−S),连接这两个集合的最小权重边一定属于某个最小生成树。

算法过程解释:

  1. 初始时,S 只有一个顶点,割是 S 与 V−S。

  2. 每次选连接 S 与 V−S 的最小权重边 e,根据割性质,这条边一定在某个 MST 中。

  3. 将 e 加入 MST,并把 e 在 V−S 中的那个顶点加入 S。

  4. 重复直到 S=V,此时所有加入的边构成一棵树,且每一步都符合 MST 的边,因此最终得到的就是 MST。



可以把上面的性质换成下面的例子?


想象有 4 个岛屿(A、B、C、D),它们之间可以造桥,但造桥的成本不同。
我们想用最低总成本造桥,让所有岛屿连通(可以互相到达),并且不造多余的桥(避免环)。

Prim 算法的思路

  1. 随便选一个岛作为起点(比如 A)。

    • 现在“已连通区” = {A},其他岛是“未连通区”。

  2. 从已连通区 造桥到 未连通区,选最便宜的那座桥。

    • 为什么必须这样选?
      因为最终所有岛都要连通,迟早要造一座桥从已连通区通往某个未连通岛。
      如果现在不选最便宜的,以后还是要造桥过去,但那时可能更贵,总成本就高了。
      所以现在选最便宜的桥不会错。

  3. 桥造好后,那个新岛就加入“已连通区”。

    • 重复第 2 步,直到所有岛都连通。


五、参考代码

  1. 朴素解法 (临接矩阵)

#include <iostream>
#include <climits>
using namespace std;
const int V = 5;  // 图的顶点数量(可按需修改,有类型检查,更安全)
// 函数功能:找到未加入最小生成树(MST)且权重最小的顶点
//   key[]: 数组,存储每个顶点到当前生成树的最小权重值
//   mstSet[]: 布尔数组,标记顶点是否已加入最小生成树(true=已加入,false=未加入)
// 返回值:返回未加入MST且权重最小的顶点的索引
int minKey(int key[], bool mstSet[]) {
    // 初始化最小值为无穷大,最小顶点索引暂未确定
    int min = INT_MAX, min_index;
    // 遍历所有顶点,寻找符合条件的最小权重顶点
    for (int v = 0; v < V; v++) {
        // 条件1:顶点v未加入MST(mstSet[v]为false)
        // 条件2:顶点v的当前最小权重小于当前记录的最小值
        if (!mstSet[v] && key[v] < min) {
            min = key[v];       // 更新最小值为当前顶点的权重
            min_index = v;      // 记录最小权重顶点的索引
        }
    }
    return min_index;  // 返回找到的最小权重顶点索引
}
// 函数功能:打印最小生成树的边、对应权重,以及总权重
//   parent[]: 数组,存储每个顶点在生成树中的父节点(用于确定边的两个端点)
//   graph[V][V]: 存储图的邻接矩阵,graph[i][j]表示顶点i到j的边权重(0表示无边)
void printMST(int parent[], int graph[V][V]) {
    // 输出表头,\t表示制表符,让格式更整齐
    cout << "边 \t权重\n";
    int totalWeight = 0;  // 定义变量,累加生成树的总权重
    // 遍历第1到第V-1个顶点(第0个顶点是根节点,无父节点,无需遍历)
    for (int i = 1; i < V; i++) {
        // 输出:父节点 - 当前节点  对应的边权重
        cout << parent[i] << " - " << i << " \t" << graph[i][parent[i]] << "\n";
        // 累加每条边的权重到总权重
        totalWeight += graph[i][parent[i]];
    }
    // 输出生成树的总权重
    cout << "总权重: " << totalWeight << endl;
}

// 函数功能:Prim算法核心实现(邻接矩阵版),构建最小生成树
// 参数说明:graph[V][V] 输入的图的邻接矩阵,无向图满足graph[i][j] = graph[j][i]
void primMST(int graph[V][V]) {
    int parent[V];      // 存储最小生成树的结构:parent[i]表示顶点i在MST中的父节点
    int key[V];         // 键值数组:key[i]表示顶点i到当前MST的最小权重边的权重值
    bool mstSet[V];     // 标记数组:mstSet[i]=true 表示顶点i已加入MST
    
    // 初始化:所有顶点的key值设为无穷大(表示初始时到MST无连接),且都未加入MST
    for (int i = 0; i < V; i++) {
        key[i] = INT_MAX;   // 无穷大表示暂时没有到MST的边
        mstSet[i] = false;  // 初始状态所有顶点都不在MST中
    }
    
    // 选择第0个顶点作为MST的起点(根节点)
    key[0] = 0;         // 起点到自身的权重为0(无成本)
    parent[0] = -1;     // 根节点没有父节点,用-1标记
    
    // MST有V个顶点,需要V-1条边,因此循环V-1次来选择边
    for (int count = 0; count < V - 1; count++) {
        // 步骤1:找到未加入MST且key值最小的顶点u
        int u = minKey(key, mstSet);
        // 步骤2:将顶点u加入MST
        mstSet[u] = true;
        
        // 步骤3:更新顶点u的所有相邻顶点的key值和父节点
        for (int v = 0; v < V; v++) {
            // 条件说明:
            // 1. graph[u][v] != 0:顶点u和v之间有边相连(邻接矩阵中0表示无边)
            // 2. !mstSet[v]:顶点v未加入MST
            // 3. graph[u][v] < key[v]:u-v这条边的权重比v当前的最小权重更小
            if (graph[u][v] && !mstSet[v] && graph[u][v] < key[v]) {
                parent[v] = u;          // 更新v的父节点为u
                key[v] = graph[u][v];   // 更新v到MST的最小权重为u-v边的权重
            }
        }
    }
    // 打印最终生成的最小生成树
    printMST(parent, graph);
}

int main() {
    // graph[i][j] = w 表示顶点i到j有一条权重为w的无向边;0表示i和j之间无边
    // 例如:graph[0][1]=2 表示顶点0和1之间有一条权重为2的边
    int graph[V][V] = {
        {0, 2, 0, 6, 0},  // 顶点0的邻边:0-1(权重2)、0-3(权重6),其余无边
        {2, 0, 3, 8, 5},  // 顶点1的邻边:1-0(2)、1-2(3)、1-3(8)、1-4(5)
        {0, 3, 0, 0, 7},  // 顶点2的邻边:2-1(3)、2-4(7),其余无边
        {6, 8, 0, 0, 9},  // 顶点3的邻边:3-0(6)、3-1(8)、3-4(9),其余无边
        {0, 5, 7, 9, 0}   // 顶点4的邻边:4-1(5)、4-2(7)、4-3(9),其余无边
    };
    // 输出提示信息
    cout << "Prim算法生成的最小生成树:\n";
    // 调用Prim算法函数,生成并打印最小生成树
    primMST(graph);
    return 0;
}

2.优先队列解法

#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;

// pii 表示 (边权重, 顶点编号) 的键值对,
typedef pair<int, int> pii;  

// 全局变量:存储图的核心信息(替代原Graph类的成员变量)
int V;                      // 图的顶点总数
vector<vector<pii>> adj;    // 邻接表:adj[u] 存储 (相邻顶点v, 边权重w) 的列表

// 初始化图
// n表示图的顶点总数
void initGraph(int n) {
    V = n;
    adj.resize(V);  // 初始化邻接表大小为顶点数
}
// 添加无向边
//   u: 起点编号
//   v: 终点编号
//   w: 边的权重
void addEdge(int u, int v, int w) {
    adj[u].push_back({v, w});  // 向u的邻接表添加v和权重w
    adj[v].push_back({u, w});  // 无向图:v的邻接表也要添加u和权重w
}

// 优先队列(最小堆)优化的Prim算法,构建最小生成树(MST)
void primMST() {
    // 优先队列(最小堆):存储 (权重, 顶点),堆顶始终是权重最小的元素
    // greater<pii> 表示按权重升序排列,实现最小堆(默认是最大堆)
    priority_queue<pii, vector<pii>, greater<pii>> pq;
    
    vector<int> key(V, INT_MAX);      // key[v]:顶点v到当前MST的最小权重,初始为无穷大
    vector<int> parent(V, -1);        // parent[v]:顶点v在MST中的父节点,初始为-1(无父节点)
    vector<bool> inMST(V, false);     // inMST[v]:标记顶点v是否已加入MST,初始为false
    
    // 选择顶点0作为MST的起点
    pq.push({0, 0});  // 起点权重为0,加入优先队列
    key[0] = 0;       // 起点到自身的权重设为0

    // 遍历优先队列,直到所有顶点加入MST
    while (!pq.empty()) {
        // 步骤1:取出堆顶元素(权重最小的顶点)
        int u = pq.top().second;  // 获取顶点编号(second是顶点,first是权重)
        pq.pop();                // 弹出堆顶
        
        // 跳过已加入MST的顶点(避免重复处理)
        if (inMST[u]) continue;
        inMST[u] = true;         // 将顶点u标记为已加入MST
        
        // 步骤2:遍历顶点u的所有相邻顶点,更新权重
        for (auto &neighbor : adj[u]) {
            int v = neighbor.first;    // 相邻顶点编号
            int weight = neighbor.second;  // u-v边的权重
            
            // 条件:v未加入MST 且 u-v边的权重 < v当前的最小权重
            if (!inMST[v] && key[v] > weight) {
                key[v] = weight;       // 更新v到MST的最小权重
                pq.push({key[v], v});  // 将新的权重和顶点加入优先队列
                parent[v] = u;         // 更新v的父节点为u
            }
        }
    }
    
    // 步骤3:打印MST的边、权重和总权重
    cout << "边 \t权重\n";
    int totalWeight = 0;  // 存储MST的总权重
    // 遍历1~V-1顶点(0是根节点,无父节点)
    for (int i = 1; i < V; i++) {
        cout << parent[i] << " - " << i << " \t" << key[i] << "\n";
        totalWeight += key[i];  // 累加每条边的权重
    }
    cout << "总权重: " << totalWeight << endl;
}

int main() {
    // 初始化图:5个顶点的无向带权图(替代原Graph g(5))
    initGraph(5);
    
    // 依次添加边(起点, 终点, 权重)
    addEdge(0, 1, 2);
    addEdge(0, 3, 6);
    addEdge(1, 2, 3);
    addEdge(1, 3, 8);
    addEdge(1, 4, 5);
    addEdge(2, 4, 7);
    addEdge(3, 4, 9);
    
    // 输出提示信息并调用Prim算法
    cout << "Prim算法(优先队列优化)生成的最小生成树:\n";
    primMST();
    
    return 0;
}


扫描二维码推送至手机访问。

版权声明:本文由青少年编程知识记录发布,如需转载请注明出处。

分享给朋友:

相关文章

C++ 如何隐藏光标

在C++控制台做小游戏的时候,光标一直在闪,影响体验效果,我们可以通过下面的函数隐藏光标位置。void HideCursor(){ CONSOLE_CURSOR_INFO cu...

图的访问与遍历-深度优先搜索

图的访问与遍历-深度优先搜索

一、图的遍历图的遍历是指从图中的某个顶点出发,按照一定规则访问图中所有顶点且每个顶点仅访问一次的过程,核心分为深度优先搜索(DFS) 和广度优先搜索(BFS) 两大类,适用于无向图...

STL入门——容器3:map

一、定义    Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据&nb...

C++小项目——实时钟表

C++小项目——实时钟表

0.前言在很多游戏中,我们要使用时间,这个时间一个是系统当前的时间,一个是服务器给你的时间。这篇文章我们尝试做一个模拟时钟。效果图如下1.任务分解1.首先我们尝试使用easyx来画一个。基本框架如下:...

【C++图形化编程】鼠标函数及鼠标画板

【C++图形化编程】鼠标函数及鼠标画板

0.前言这篇文章简单介绍一下利用鼠标画图的程序#include<graphics.h> #include<conio.h> int main(){ initg...

【数据结构】队列—基本操作

【数据结构】队列—基本操作

一、C++实例分析       C++队列queue模板类的定义在<queue>头文件中,queue 模板类需要两个模板参数,一个是元素类型,一个容...