最小生成树—Prim(普里姆)算法
一、算法概述
Prim 算法是一种用于求解加权无向连通图的最小生成树(MST) 的贪心算法。它从一个顶点开始,逐步扩展生成树,每次选择连接已选顶点集和未选顶点集的最小权重边。
二、算法思想
初始化:从任意顶点开始,将其加入生成树集合
重复执行:
在所有连接已选顶点和未选顶点的边中,选择权重最小的边
将该边加入生成树,并将对应的未选顶点加入已选集合
终止条件:当所有顶点都加入生成树时结束
三、算法过程示例
示例图(顶点: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,一端不在 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),连接这两个集合的最小权重边一定属于某个最小生成树。
算法过程解释:
初始时,S 只有一个顶点,割是 S 与 V−S。
每次选连接 S 与 V−S 的最小权重边 e,根据割性质,这条边一定在某个 MST 中。
将 e 加入 MST,并把 e 在 V−S 中的那个顶点加入 S。
重复直到 S=V,此时所有加入的边构成一棵树,且每一步都符合 MST 的边,因此最终得到的就是 MST。
可以把上面的性质换成下面的例子?
想象有 4 个岛屿(A、B、C、D),它们之间可以造桥,但造桥的成本不同。
我们想用最低总成本造桥,让所有岛屿连通(可以互相到达),并且不造多余的桥(避免环)。
Prim 算法的思路
随便选一个岛作为起点(比如 A)。
现在“已连通区” = {A},其他岛是“未连通区”。
从已连通区 造桥到 未连通区,选最便宜的那座桥。
为什么必须这样选?
因为最终所有岛都要连通,迟早要造一座桥从已连通区通往某个未连通岛。
如果现在不选最便宜的,以后还是要造桥过去,但那时可能更贵,总成本就高了。
所以现在选最便宜的桥不会错。桥造好后,那个新岛就加入“已连通区”。
重复第 2 步,直到所有岛都连通。
五、参考代码
朴素解法 (临接矩阵)
#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;
}扫描二维码推送至手机访问。
版权声明:本文由青少年编程知识记录发布,如需转载请注明出处。




