最小生成树 Prim算法及其堆优化

先来说一下Prim算法的基本思路:

1.任意找到图中的一个顶点,作为最小生成树的根,进行标记(已被加入树)开始扩展

2.从已扩展的结点中找到权值最小的点(已被扩展,可以保证其与生成树是连通的),记录并标记。

3.将与该点相连通的所有结点进行扩展

4.循环执行2,3步,直到所有结点都被加入到生成树

为此,需要维护几个变量来保存已有状态和记录当前状态:

color[MAX]; 用于记录所有结点的访问状态(对应算法思路第1,2步中的标记),WHITE表示未被访问,BLACK表示已被加入生成树

MAP[MAX][MAX];邻接矩阵,用于记录图中连通结点之间的权值

d[MAX]; 用于记录权值最小边的权值   eg.d[v]表示点v与生成树连通的最小权值

p[MAX]; 用于记录生成树中各个结点的父节点  eg.p[v]表示生成树中v的父节点

下面看代码:

#include <iostream>
#include <algorithm>
#include <cstdlib>
#define max 1<<10
#define INF 1<<12
#define white 1
#define black 0
using namespace std;

int n,m;
int path[max][max];

int prim(){
	int p[max];
	int d[max];
	int color[max];
	int mink;
	int u;
	for(int i=0;i<m;i++){
		p[i]=-1;
		d[i]=INF;
		color[i]=white;
	}
	u=0;
	d[u]=0;
	p[0]=INF;
	while(true){
		u=-1;
		mink=INF;
		for(int i=0;i<m;i++){//找出扩展的点 
			if(d[i]<mink&&color[i]!=black){
				u=i;
				mink=d[i];
			}
		}
		if(u==-1)break;//没有找到点,说明都已被扩展
		color[u]=black;
		for(int i=0;i<m;i++){//扩展该点 
			if(color[i]!=black&&path[u][i]!=INF){//i点可扩展且存在与u的路径 
				if(path[u][i]<d[i]){
					d[i]=path[u][i];
					p[i]=u;
				}
			}
		}
	}
	int result=0;
	for(int i=0;i<m;i++){
		if(p[i]!=INF) 
		result+=path[i][p[i]];
	}
	return result;
}

以上是最经典的Prim算法,使用邻接矩阵实现。

对于复杂度:我们需要遍历图的所有顶点来确定d最小的顶点u,且整个算法的遍历次数与顶点数相等,因此算法复杂度为     O(|V|²)。

对于小规模数据,可以用此算法解决,但对于数据较大的,在算法竞赛中就会超时。

下面举一道题为例:

洛谷P3366:

题目描述

如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出orz

输入输出格式

输入格式:

第一行包含两个整数N、M,表示该图共有N个结点和M条无向边。(N<=5000,M<=200000)

接下来M行每行包含三个整数Xi、Yi、Zi,表示有一条长度为Zi的无向边连接结点Xi、Yi

输出格式:

输出包含一个数,即最小生成树的各边的长度之和;如果该图不连通则输出orz

其中:

对于20%的数据:N<=5,M<=20

对于40%的数据:N<=50,M<=2500

对于70%的数据:N<=500,M<=10000

对于100%的数据:N<=5000,M<=200000

我们看到,对于100%,使用复杂度O(|V|²)的算法很可能会超时。因此需要对算法进行优化

分析算法,我们很容易了解到,时间主要花费在了挑选最小权值点和扩展上,对于挑选最小权值点,我们可以用通过维护一个优先队列来加快我们的取点速度。而对于扩展,我们可以用邻接表的存储方式,减少对无权点(不连通点)遍历的时间

下面是具体的题解代码:

#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <queue>
#include <vector>
#define INF 1<<12
#define white 1
#define gray 2
#define black 3

using namespace std;


int n, m;
int path[5005][5005];
int p[5005];
int d[5005];
int color[5005];
int result;

struct node {
	int loc;
	int len;
};

struct comp {
	bool operator()(node a, node b) {
		return a.len>b.len;
	}
};

vector<node> road[5005];//邻接表
int sign;

void prim() {

	int min;
	int u;
	priority_queue<node, vector<node>, comp> buf;//记录扩展出去的最小路径的堆 
	d[0] = 0;
	p[0] = 0;
	node temp = { 0,0 };
	buf.push(temp);
	while (!buf.empty()) {
		u = buf.top().loc;//用优先队列减少取点时间
		//min=buf.top().len;
		buf.pop();
		color[u] = black;


		for (int i = 0; i<road[u].size(); i++) {//邻接表取点,减少对无权点的遍历
			int v = road[u][i].loc;
			if (color[v] == black)continue;
			int templen = road[u][i].len;
			if (templen<d[v]) {
				d[v] = templen;
				p[v] = u;
				temp.len = d[v];
				temp.loc = v;
				buf.push(temp);
			}
		}
	}
	for (int i = 1; i<n; i++) {
		if (p[i] == -1) {//判断是否连通
			sign = 1;
			break;
		}
		result += d[i];
	}
	return;
}

int main() {
	cin >> n >> m;
	node a;
	for (int i = 0; i<m; i++) {
		int x, y, z;
		cin >> x >> y >> z;
		path[x-1][y-1] = z;
		a.loc = y-1;
		a.len = z;
		road[x-1].push_back(a);
		path[y-1][x-1] = z;
		a.loc = x-1;
		a.len = z;
		road[y-1].push_back(a);
	}
	for (int i = 0; i<n; i++) {
		p[i] = -1;
		d[i] = INF;
		color[i] = white;
	}
	prim();
	if (sign)cout << "orz" << endl;
	else {
		cout << result << endl;
	}
	return 0;
}

本题有一个要求是判断是否连通,对此,只要最后遍历每个结点,检查其根是否是生成树内结点即可完成。

猜你喜欢

转载自blog.csdn.net/BrightHao_zi/article/details/84728819