第2节——再谈最小生成树(Prim算法)

算法流程

如果说上个算法基于点,那这个Prim算法就是基于边的。
算法流程大致如下:
1.从任意一个人顶点开始构造生成树,假设从1号顶点开始。首先将1号顶点加入生成树中,用一个一维数组book来标记那些顶点已经加入了生成树。
2.用数组dis记录生成树到各个顶点的距离。最初生成树中只有1号顶点,有直连边时,数组dis中存储的就是1号顶点到该顶点的边的权值,没有直连边时就是无穷大,即inf。
3.从数组dis中选出离生成树最近的顶点(假设这个顶点为j)加入到生成树中(即在数组dis中找最小值)。再以j为中间点,更新生成树到每一个非树顶点的距离(就是松弛啦),即如果dis[k] > e[j][k]则更新dis[k] = e[j][k]。
4.重复第3步,直到生成树中有n个顶点为止。

代码

#include <stdio.h>
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f//用inf定义为我们认为的一个正无穷的值 

int main(){
    
    
	int n, m, i, j, k, min, t1, t2, t3;
	int e[7][7], dis[7], book[7] = {
    
    0};//对book数组进行了初始化 
	int count = 0, sum = 0;//count用来记录生成树中顶点的个数,sum用来存储路径之和
	
	//读入n和m,n表示顶点个数,m表示边的条数
	cin >> n >> m;
	
	//初始化
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			if(i == j) e[i][j] = 0;
			else e[i][j] = inf; 
	
	//开始读入边
	for (int i = 1; i <= m; ++i){
    
    
		cin >> t1 >> t2 >>t3;
		//注意这里是无向图,所以需要将边反向再存储一遍
		e[t1][t2] = t3;
		e[t2][t1] = t3; 
	} 
	
	//初始化dis数组,这里是1号顶点到各个顶点的初始距离,因为当前生成树中只有1号顶点
	for (int i = 1; i <= n; ++i){
    
    
		dis[i] = e[1][i];
	} 
	
	//Prime算法核心部分开始
	//将1号顶点加入生成树
	book[1] = 1;//这里用一个book来标记一个顶点是否已经加入生成树 
	count ++;//count 是最小生成树中各顶点的个数,因为1号顶点已经入队所以最小生成树中顶点的个数+1 
	while(count < n){
    
    
		min = inf;
		for(i = 1; i <= n; ++i){
    
    
			if(book[i] == 0 && dis[i] < min){
    
    
				min = dis[i];
				j = i;
			}
		}
		book[j] = 1;
		count ++;
		sum += dis[j];
		
		//扫描当前顶点j所有的边,再以j为中间点,更新生成树到每一个非树顶点的距离
		for (k = 1; k <= n; ++k){
    
    
			if(book[k] == 0 && dis[k] > e[j][k])
				dis[k] = e[j][k];
		}  
	} 
	
	cout << sum;
	
	return 0;
}

测试样例:

6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2

运行结果:

19

优化

上面这种算法的时间复杂度是O(N2)。如果借助“堆”,每次选边的时间复杂度是O(logM),然后使用邻接表来存储图的话,整个算法的时间复杂度会降低到O(MlogN)。那么究竟如何优化呢?我们需要三个数组,如下图:
在这里插入图片描述
数组dis用来记录生成树到各个顶点的距离。数组h是一个最小堆,堆里面存储的是顶点编号。请注意,这里并不是按照顶点编号的大小来建立最小堆的,而是按照顶点在数组dis中对应的值来建立这个最小堆。此外还需要一个pos数组来记录每个顶点在最小堆中的位置。例如上图中,左边最小堆的圆圈中存储的是顶点编号,圆圈右下角的数是该顶点(圆圈里面的数)到生成树的最短距离,即数组dis中存储的值。

//Prim算法优化版
#include <stdio.h>
#include <iostream>
using namespace std;
#define inf 0x3f3f3f3f

int dis[7], book[7]= {
    
    0};//book数组用来记录哪些顶点已经放入生成树中
int h[7], pos[7], size;//h用来保存堆,pos用来存储每个顶点在堆中的位置,size为堆的大小。

void swap(int x, int y){
    
    
	int t;
	t = h[x];
	h[x] = h[y];
	h[y] = t;
	
	//同步更新pos
	t = pos[h[x]];
	pos[h[x]] = pos[h[y]];
	pos[h[y]] = t; 
	return ;
} 

void siftdown(int i){
    
    //传入一个需要继续向下调整的节点编号
	int t, flag = 0;//flag用来标记是否需要向下调整
	while(i*2 <= size && flag == 0){
    
    
		//比较i和它的左儿子i*2在dis中的值,并用t记录较小的点的编号
		if(dis[h[i]] > dis[h[i*2]]) t = i*2;
		else t = i;
		//如果它有右儿子,再对右儿子进行讨论,并用t记录较小的节点编号
		if(i*2+1 <= size){
    
    
			//如果右儿子的值更小,更新较小的节点编号 
			if(dis[h[t]] > dis[h[i*2+1]]) t = i*2 + 1; 
		} 
		//如果发现最小的节点编号不是自己,说明子节点中有比父节点更小的
		if(t != i){
    
    
			swap(t, i);//交换他们 
			i = t;//更新i为刚才与它交换的儿子节点的编号,便于接下来继续向下调整 
		} 
		else flag = 1;//否则说明当前的父节点已经比两个子节点都要小了,不需要再进行调整了。 
	} 
	return ; 
}

void siftup(int i){
    
    //传入一个需要向上调整的节点编号i 
	int flag = 0;//用来标记是否需要向上调整
	if(i == 1) return ;//如果是堆顶就已经不需要向上调整了。
	//不在堆顶,并且当前节点i的值比父节点小的时候就继续向上调整 
	while(i != 1 && flag == 0){
    
    
		//判断是否比父节点的小
		if(dis[h[i]] < dis[h[i/2]])
			swap(i, i/2);//交换它和它爸爸的位置
		else
		 	flag = 1;//表示已经不需要调整了,当前节点的值比父节点的值要大
		i = i / 2; //这句话很重要,更新编号i为它父节点的编号,从而便于下一次继续向上调整 
	}	
	return ; 
}

//从堆顶取出一个元素
int pop(){
    
    
	int t;
	t = h[1];//用一个临时变量及记录堆顶点的值
	pos[t] = 0;//其实这句话要不要无所谓 
	h[1] = h[size];	//将堆的最后一个点赋值到堆顶
	pos[h[1]] = 1;
	size--;//将堆的元素减少一
	siftdown(1);//向下调整1 
	return t; 
} 

int main(){
    
    
	int n, m, i, j, k;
	//u、v、w和next数组的大小要根据实际情况来设置,此图是无向图,要比2*m的最大值要大1
	//first要比n的最大值要大1
	int u[19], v[19], w[19], first[7], next[19];
	int count = 0, sum = 0;//count用来记录生成树中顶点的个数,sum用来存储路径之和。
	
	//读入n和m,n表示顶点个数,m表示边的个数
	cin >> n >> m;
	
	for (int i = 1; i <= m; ++i){
    
    
		cin >> u[i] >> v[i] >> w[i];
	} 
	
	//这里是无向图,所以需要将所有的边再反向存储一次
	for (int i = m + 1; i <= 2 * m; ++i){
    
    
		u[i] = v[i - m];
		v[i] = u[i - m];
		w[i] = w[i - m];
	} 
	
	//开始使用邻接表存储边
	for (int i = 1; i <= n; ++i){
    
    
		first[i] = -1;
	} 
	
	for (int i = 1; i <= 2*m; ++i){
    
    
		next[i] = first[u[i]];
		first[u[i]] = i;
	}
	
	//Prim核心部分开始
	//将1号顶点加入生成树
	book[1] = 1; //这里用book数组来标记一个顶点已经加入生成树
	count ++;
	
	//初始化dis数组,这里是1号顶点到其余各个顶点的初始距离
	dis[1] = 0;
	for (int i = 2; i <= n; ++i){
    
    
		dis[i] = inf;
	} 
	k = first[1];
	while(k != -1){
    
    
		dis[v[k]] = w[k];
		k = next[k];
	}
	
	//初始化堆 
	size = n; 
	for (int i = 1; i <= size; ++i){
    
    
		h[i] = i;
		pos[i] = i;
	}
	for (i = size/2; i >= 1; --i) {
    
    siftdown(i);}
		pop();//先弹出一个堆顶元素,因为此时堆顶是1号顶点。 
	
	while(count < n){
    
    
		j = pop();
		book[j] = 1;
		count ++;
		sum = sum + dis[j];
		
		//扫描当前顶点j所有的边,再以j为中间节点,进行松弛
		k = first[j];
		while(k != -1){
    
    
			if(book[v[k]] == 0 && dis[v[k]] > w[k]){
    
    
				dis[v[k]] = w[k];//更新距离 
				siftup(pos[v[k]]);//对该点在堆中进行向上调整
				//提示:pos【v[k]】存储的是顶点v[k]在堆中的位置 
			}
			k = next[k]; 
		} 
	}
	
	cout << sum;
	return 0;
}  

Guess you like

Origin blog.csdn.net/LXC_007/article/details/116131992