prim 算法 c++实现

本文详细介绍了网络的最小生成树概念及其在通信网络设计中的应用,阐述了最小生成树性质,并深入解析了PRIM算法的工作原理及实现过程,包括关键数据结构的设计和算法的时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.概述

  设G =(V,E)是无向连通带权图,即一个网络。E中每条边(v,w)的权为c[v][w]。如果G的子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中,耗费最小的生成树称为G的最小生成树

     网络的最小生成树在实际中有广泛应用。例如,在设计通信网络时,用图的顶点表示城市,用边(v,w)的权c[v][w]表示建立城市v和城市w之间的通信线路所需的费用,则最小生成树就给出了建立通信网络的最经济的方案。

  最小生成树性质:

   设G=(V,E)是连通带权图,U是V的真子集。如果(u,v)属于E,且u属于U,v属于V-U,且在所有这样的边中,(u,v)的权c[u][v]最小,那么一定存在G的一棵最小生成树,它以(u,v)为其中一条边。这个性质有时也称为MST性质。

 

2.PRIM算法

  设G=(V,E)是连通带权图,V={1,2,…,n}。

  构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件i属于S,j属于V-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。

  在这个过程中选取到的所有边恰好构成G的一棵最小生成树。

  利用最小生成树性质和数学归纳法容易证明,上述算法中的边集合T始终包含G的某棵最小生成树中的边。因此,在算法结束时,T中的所有边构成G的一棵最小生成树。

  例如,对于右图中的带权图,按Prim算法选取边的过程如下页图所示。  

按Prim算法选取边的过程如下页图所示。

  在上述Prim算法中,还应当考虑如何有效地找出满足条件i在S中,j也在V-S中,且权c[i][j]最小的边(i,j)。实现这个目的的较简单的办法是设置2个数组closest和lowcost。

  在Prim算法执行过程中,先找出V-S中使lowcost值最小的顶点j,然后根据数组closest选取边(j,closest[j]),最后将j添加到S中,并对closest和lowcost作必要的修改。

  用这个办法实现的Prim算法所需的计算时间为O(n2).

       其实这个算法和迪杰斯特拉算法非常相似,都是每次都要取目前已知的到达未连通结点的最小权值,当加入该点之后再刷新lowcost数组和closest数组,对比看是否未加入树的点通过该刚加入树的点是否有更短的路径也就是更小的权值,若有则刷新,若没有则不处理,以保证lowcost和closest数组里的数据都是根据当前已知情况得到的最小权值数组和该顶点最接近的顶点编号。

图是搬运的,因为自己懒得做。

代码如下:

#include<iostream>
#include<vector>
using namespace std;
void prim(vector<vector<int>> &VGraph, vector<int> &lowcost, vector<int> &closest, vector<bool> &visited)  //思路类似于迪杰斯特拉算法
{
	int size = lowcost.size();
	visited[0] = true;
	for (int i = 1; i < size; i++)
	{
		lowcost[i] = VGraph[0][i];
		closest[i] = 0;
		visited[i] = false;
	}
	cout << "0";
	int weight = 0;
	for (int i = 0; i < size; i++)
	{
		int min = 99999;
		int index = 1;
		for (int j = 0; j < size; j++)
		{
			if (lowcost[j] < min&&!visited[j])
			{
				min = lowcost[j];
				index = j;
			}
		}
		if (index == 1 && min == 99999)
		{
			cout << "\n最小生成树权值为:" << weight<<endl;
			return;
		}
		else
		{
			weight += min;
		}
		cout << " -> " << index;
		visited[index] = true;
		for (int j = 1; j <size; j++)  //因为新加入了j点,所以要查找新加入的j点到未在S中的点K中的权值是不是可以因此更小
		{
			if ((VGraph[index][j]<lowcost[j]) && (!visited[j]))  //lowcost表示从已知树到某一点所需付出的最小权值
			{ 
				lowcost[j] = VGraph[index][j];
				closest[j] = index;
			}
		}
	}
}
int main()
{
	int M, N;
	cin >>M>>N;
	vector<vector<int>> VGraph(M);
	vector<int> lowcost(M);
	vector<int> closest(M);
	vector<bool> visited(M);
	for (int i = 0; i < M; i++)
	{
		VGraph[i].resize(M);
	}
	for (int i = 0; i < M; i++)
	{
		for (int j = 0; j < M; j++)
		{
			VGraph[i][j] = 99999;
		}
	}
	for (int i = 0; i < N; i++)
	{
		int a, b;
		cin >> a >> b;
		int length;
		cin >> length;
		VGraph[a][b] = VGraph[b][a] = length;
	}
	prim(VGraph, lowcost, closest, visited);
}

 

<think>好的,我现在要解决的是如何用C++实现Prim算法Prim算法是用来生成一个带权无向图的最小生成树的。我得先回忆一下Prim算法的基本步骤,然后考虑怎么用C++实现它。 首先,Prim算法的核心思想是从一个起始点开始,逐步扩展生成树,每次选择连接树和非树节点的最小权重的边。具体步骤应该是这样的:选择一个起始顶点,然后维护一个优先队列(最小堆)来保存所有连接已选顶点和未选顶点的边,每次取出权重最小的边,将对应的顶点加入生成树,并把这个顶点的所有邻接边加入队列,但要排除已经加入生成树的顶点。重复这个过程,直到所有顶点都被包含在生成树中,或者无法形成生成树。 接下来,我需要确定数据结构。图可以用邻接表或者邻接矩阵来表示。邻接表可能更节省空间,特别是对于稀疏图。这里可能需要定义一个结构体或者用pair来保存边的终点和权重。比如,可以用一个vector<vector<pair<int, int>>>来表示,其中每个顶点对应一个vector,里面存的是与之相连的顶点和边的权重。 然后是优先队列,C++中有priority_queue这个容器,但默认是最大堆,而我们需要最小堆。因此,需要将优先队列的第三个参数改为greater,或者是在元素插入时取负数,取出时再转换回来。比如,priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>>这样的方式,这样就能按边的权重从小到大排序了。 另外,还需要一个数组或者集合来记录哪些顶点已经被加入到生成树中,避免重复访问。比如,可以用一个bool类型的数组visited,初始时都为false,当顶点被加入生成树时标记为true。 然后,算法的具体步骤大致如下: 1. 初始化:选择起始顶点,比如顶点0,将visited[0]设为true,并将所有从0出发的边加入优先队列。 2. 循环,直到所有顶点都被访问或者优先队列为空: a. 取出队列中权重最小的边(即堆顶元素)。 b. 如果该边连接的顶点已经被访问过,跳过。 c. 否则,将该顶点标记为已访问,并将这条边加入最小生成树的总权重中。 d. 将该顶点的所有邻接边中未被访问的顶点对应的边加入优先队列。 3. 最后,检查是否所有顶点都被访问过,如果是,则输出总权重;否则说明图不连通,无法形成最小生成树。 需要注意的是,在将边加入优先队列时,要确保边的另一端顶点未被访问。否则可能会重复添加边或者导致循环。 现在,可能的错误点或者需要注意的地方是: - 优先队列的比较方式是否正确,确保每次取出的是当前最小的边。 - 在添加邻接边到队列时,必须检查邻接顶点是否已经被访问,否则可能导致队列中存在大量无效边,影响效率。 - 如果图是非连通的,那么最终无法形成包含所有顶点的最小生成树,此时应该给出提示或者返回一个错误值。 举个例子,假设图的顶点是0到n-1,邻接表的每个元素是pair<int, int>,其中第一个int是邻接顶点,第二个是权重。 代码的大体结构可能如下: 首先,构建图的邻接表。例如: vector<vector<pair<int, int>>> graph(n); 然后,初始化visited数组,大小为n,初始为false。 priority_queue存储的是边,这里可能需要存权重和顶点。比如,优先队列中的元素是pair<int, int>,其中第一个元素是权重,第二个是顶点。这样,优先队列会根据权重自动排序。 然后,从顶点0开始,标记为已访问,遍历其邻接边,将权重和对应的顶点加入队列。 然后,进入循环,取出最小边,检查顶点是否已访问。如果没访问,则累加权重,并将该顶点的邻接边加入队列。 例如,代码的大致步骤: int prim() { int n = graph.size(); vector<bool> visited(n, false); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; visited[0] = true; for (auto &edge : graph[0]) { pq.push({edge.second, edge.first}); // 权重在前,顶点在后 } int minCost = 0; int edgesUsed = 0; while (!pq.empty()) { auto current = pq.top(); pq.pop(); int weight = current.first; int u = current.second; if (visited[u]) continue; visited[u] = true; minCost += weight; edgesUsed++; for (auto &edge : graph[u]) { int v = edge.first; int w = edge.second; if (!visited[v]) { pq.push({w, v}); } } } if (edgesUsed == n - 1) { // 最小生成树有n-1条边 return minCost; } else { return -1; // 图不连通 } } 这里需要注意,初始时visited[0]设为true,然后edgesUsed初始为0,因为每次选中一条边。最终,如果edgesUsed等于n-1,说明有足够的边构成生成树。 可能的问题:起始顶点是否有边?如果图是空的或者不连通,返回-1。 另外,关于边的存储方式是否正确。例如,邻接表中的每个pair存储的是邻接顶点和权重,那么在将边加入优先队列时,权重是edge.second,顶点是edge.first。 但是,在邻接表的定义中,假设每个顶点u的邻接边存储的是(v, weight),其中v是邻接顶点,那么这样的处理是对的。 例如,graph[u]中的每个元素是pair<int, int>,其中第一个是v,第二个是weight。 在代码中,当处理顶点u的邻接边时,对于每个edge,邻接顶点是edge.first,权重是edge.second。因此,在加入优先队列时,pq.push({edge.second, edge.first}),即权重作为优先队列的第一个元素,顶点作为第二个。这样优先队列会按照权重从小到大排列。 这样的逻辑是对的。 测试案例的话,比如一个简单的图: 顶点0连接顶点1,权重2;顶点0连接顶点2,权重1;顶点1连接顶点2,权重3。 最小生成树的总权重应该是1 +2=3?或者顶点0-2(1),顶点0-1(2),总权重是3。或者如果顶点2连接顶点1的权重更小的话? 比如这个例子,使用Prim算法的话,从0出发,先选权重1的边到顶点2,然后将顶点2的邻接边加入队列。顶点2的邻接边包括0(已访问)和1权重3。此时优先队列中有顶点1的两个边,权重2和3,所以下一步选权重2的边到顶点1。总权重是1+2=3。 是的,所以算法应该正确。 另一个需要注意的点是,在Prim算法中,是否需要处理多个边或者自环?但假设输入的图是有效的,即没有自环或者已经被处理过。 综上,这样的代码应该可以正确实现Prim算法。当然,实际应用中可能需要更详细的错误处理,或者不同的起始点选择,但通常情况下,起始点可以任意选择,这里选0作为起始点。</think>以下是使用C++实现Prim算法求无向图最小生成树的详细步骤和代码: ```cpp #include <iostream> #include <vector> #include <queue> using namespace std; int prim(const vector<vector<pair<int, int>>>& graph) { int n = graph.size(); if (n == 0) return 0; vector<bool> visited(n, false); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; // 从顶点0开始(可任意选择起始点) visited[0] = true; for (const auto& edge : graph[0]) { pq.push({edge.second, edge.first}); // 格式:(权重, 顶点) } int minCost = 0; int edgesUsed = 0; while (!pq.empty()) { auto [weight, u] = pq.top(); pq.pop(); if (visited[u]) continue; visited[u] = true; minCost += weight; edgesUsed++; for (const auto& edge : graph[u]) { int v = edge.first; int w = edge.second; if (!visited[v]) { pq.push({w, v}); } } } return (edgesUsed == n - 1) ? minCost : -1; } int main() { // 示例:构建一个带权无向图的邻接表 int n = 3; vector<vector<pair<int, int>>> graph(n); // 添加边 (双向) graph[0].emplace_back(1, 2); // 顶点0到1,权重2 graph[1].emplace_back(0, 2); graph[0].emplace_back(2, 1); // 顶点0到2,权重1 graph[2].emplace_back(0, 1); graph[1].emplace_back(2, 3); // 顶点1到2,权重3 graph[2].emplace_back(1, 3); int result = prim(graph); if (result != -1) { cout << "最小生成树总权重: " << result << endl; // 输出3 (0-2:1, 0-1:2) } else { cout << "图不连通,无法生成最小生成树" << endl; } return 0; } ``` ### 代码解析 1. **数据结构** - 使用`vector<vector<pair<int, int>>>`表示图的邻接表,每个顶点存储相邻顶点及其权重。 - `priority_queue`(最小堆)按边权重排序,格式为`(权重, 顶点)`。 2. **算法流程** - **初始化**:标记起始顶点(如0),将其邻接边加入堆。 - **循环处理**:每次取出堆顶最小边,若该顶点未访问,则累加权重并加入其邻接边到堆中。 - **终止条件**:堆空或已选边数为`n-1`(成功生成树)。 3. **复杂度** - **时间**:O(E log E),E为边数(每次堆操作O(log E),最多E次)。 - **空间**:O(E)存储边。 ### 注意事项 - **图连通性**:若最终边数不足`n-1`,返回-1表示图不连通。 - **起始点选择**:可任选起始点,不影响最终结果。 - **边的处理**:邻接表需包含双向边(无向图特性)。 此实现能高效计算连通图的最小生成树总权重,适用于稀疏图(邻接表优势)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值