C数据结构与算法-基础整理-图-05:普里姆算法详解

详解最小生成树的普里姆算法
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

0x01.关于普里姆算法

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。

0x02.生成树和最小生成树

既然普里姆算法是关于最小生成树的算法,那么什么是什么是生成树和最小生成树呢?

生成树定义:

一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和n-1条边。

简单理解生成树:

生成树简而言之就是由图去生成一颗树,这颗树包含了图的全部顶点。

那么这里应该有几个生成树应该满足的条件:

1.不能有任何闭合得部分,因为闭合了就不叫树了。

2.得包含全部的顶点。

3.满足其它树的特征。

理解最小生成树:

最小生成树,就是生成一棵树,所花费的代价最小,也就是树所包含的所有边的权值和最小。

0x03.普里姆算法代码

由于是经典算法,先看代码。

void MinSpanTree_Prim(Graph G)
{
	int min, i, j, k;
	int adjvex[MAXSIZE];//用于保存顶点下标
	int lowcost[MAXSIZE];//保存顶点之间边的权值
	lowcost[0] = 0;//初始化第一个权值为0,A加入生成树
        adjvex[0]=0;//
	for (i = 1; i < G.numv; i++)
	{
		lowcost[i] = G.edge[0][i];
		adjvex[i] = 0;
	}
	for (i = 1; i < G.numv; i++)
	{
		min = INTMAX;
		j = 1;
		k = 0;
		while (j < G.numv)
		{
			if (lowcost[j] != 0 && &lowcost[j] < min)
			{
				min = lowcost[j];
				k = j;
			}
			j++;
		}
		printf("边 %c到%c 已被纳入生成树\n", G.ver[adjvex[k]], G.ver[k]);
		lowcost[k] = 0;
		for (j = 1; j < G.numv; j++)
		{
			if (lowcost[j] != 0 && G.edge[k][j] < lowcost[j])
			{
				lowcost[j] = G.edge[k][j];
				adjvex[j] = k;
			}
		}
	}
}
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

0x04.图解代码过程

下图是一个无向网图。顶点ABCDEFGHI分别是顶点表中的0~8号元素。

 

C数据结构与算法-基础整理-图-05:普里姆算法详解_第1张图片

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

下面按照代码过程演示:

代码第4,5行创建了两个数组,一个adjvex,一个lowcost,其中adjvex用于保存顶点的下标,lowcost是整个普里姆算法的核心,用于保存相关边的权值,具体两个数组是如何存储的见下文。

代码第6,7行分别对两个数组的0号下标进行初始化,假设从A顶点开始,lowcost[0]=0,表示A已加入生成树,可以这样理解,lowcost中的某个数一旦被置为0,说明这个下标所在顶点表中的顶点被纳入生成树了,另外,adjvex[0]=0的含义是假设从A开始。

代码第8~12行的循环表示对lowcostadjvex数组赋值,lowcost所赋的值是与A有关的边的权值,adjvex所赋的值全部是0。

此时的lowcost为[0,8,32767,32767,32767,9,32767,32767,32767]

此时的adjvex全部为0.

代码第15~17行,将min设置为INTMAX,目的是找到最小权值;j设置为1,作为下一个循环的循环变量;k用来存储权值最小的顶点下标。

代码第18~26行的循环,目的是寻找lowcost中权值最小的值,和它对应的下标。

代码第27行,输出了生成树中的第一条边,此时的k表示的是lowcost中权值最小的边对应的顶点,而adjvex[k]则表示这条边的另一个顶点,在这也就是A

代码第28行lowcost[k]=0,表示顶点k已加入生成树,对于locost0的值,以后都不进行操作。

代码第29~36行,再进行一次循环,此时的k=1,那么这次循环的目的就是查找邻接矩阵中顶点B所对应的边的权值,与之前lowcost中的值比较,若有值更小,则修改lowcost中的值,并将k存入adjvex

此时的lowcost[0,0,16,32767,32767,9,14,32767,10]

此时的adjvex[0,0,1,0,0,0,1,0,1]

第一遍大循环完成,此时的生成树如图所示(红色部分):

C数据结构与算法-基础整理-图-05:普里姆算法详解_第2张图片

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

我们发现一次大循环的目的就是找到一条权值最小的边加入最小生成树。

再来看第二次大循环。

由于第一次小循环的目的是找到lowcost中权值最小的值和顶点,那么再经历第一次小循环后,k应该等于值为9对于的下标,也就是5,而adjvex[5]=0,所以此次加入的边应该是A--F。

第二次小循环的目的是改变lowcost中的值,那么再经历第二次小循环,此时k=5,循环结束后,lowcost应为[0,0,16,32767,24,0,14,32767,10],adjvex应为[0,0,1,0,5,0,1,0,1]。

此时的生成树为:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第3张图片

 

再来第三次大循环:

第一个小循环过后,找到最小权值对于的下标k为8,adjvex[8]=1,也就是说边B--I加入生成树。

第二个小循换过后,遍历k有关的边有没有比lowcost中的值小的,循环过后,lowcost应为[0,0,16,19,24,0,14,32767,0],adjvex应为[0,0,1,8,5,0,1,0,1].

此时的生成树为:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第4张图片

 

第四次大循环:

第一次小循环结束后,k=6,adjvex[6]=1,说明边B--G加入生成树。

第二次小循换结束后,lowcost为[0,0,16,19,24,0,0,17,0],adjvex为[0,0,1,8,5,0,1,6,1]

此时的生成树如图:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第5张图片

 

第五次大循环:

第一次小循换后,k=2,adjvex[2]=2,说明边B--C加入生成树。

第二次小循环后,lowcost为[0,0,0,19,24,0,0,17,0],adjvex为[0,0,1,8,5,0,1,6,1]。

此时的生成树如图:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第6张图片

 

第六次大循环:

第一次小循换后,k=7,afjvex[7]=6,说明边G--H加入生成树;

第二次小循换后,lowcost为[0,0,0,14,5,0,0,0,0],adjvex为[0,0,1,7,7,0,1,6,1]。

此时的生成树为:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第7张图片

第七次大循环:

第一次小循换后,k=5,adjvex[5]=7,说明边H--E加入生成树。

第二次小循换后,lowcost为[0,0,0,14,0,0,0,0,0],adjvex为[0,0,1,7,7,0,1,6,1]。

此时的生成树为:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第8张图片

第八次大循环:

第一次小循换,k=3,adjvex[3]=7,说明边H--D加入生成树。

第二次小循换,lowcost全为0,生成树已完成,无意义。

此时的生成树为:

C数据结构与算法-基础整理-图-05:普里姆算法详解_第9张图片

代码到此执行完毕,最小生生成树构造完成。

一个大循环里面两个小循环,时间复杂度应为 O(e)=2e*e;

0x05.原理深究

1.关于lowcost,adjvex数组到底怎么理解。

lowcost是整个普里姆算法的核心部分,它的含义就是存储各边的权值,为0就代表,该下标所对应的顶点已加入生成树,不需要再考虑,这个权值应该这样理解,是对于已加入这棵生成树的顶点的所有边的最小权值,这个最小指的是和这个权值下标连成的边的最小权值。

例如,当第一次循环的时候,lowcost为[0,8,32767,32767,32767,9,32767,32767,32767]第一个0代表第一个顶点已加入生成树,第二个8代表,第二个顶点到已加入生成树的所有顶点之中权值最小的值,那么如何知道在第二个小循换中,改变了的话,是对于第几个顶点来说的,这个时候adjvex[k]的值就是对于第几个顶点而言,比如,此时的adjvex[2]=1,代表8这个值是对于第二个顶点来说,所有已加入生成树的顶点到这个顶点的最小权值。

lowcost每次在第一次小循换中,会找出一个最小值出来,并找到对应的顶点加入生成树。这个过程实际上就是找到所有顶点权值最小里面的最小。

lowcost中每有一个为0就是有一个顶点加入了生成树,当所有顶点都加入后,lowcost中的值都为0。

adjvex是存储相关顶点的下标的,那么这个顶点下标究竟指的是什么呢?我们可以看它是什么时候改变的呢,就是在第二个小循换才开始改变的,每次改变存的是一个权值对应的下标,所以可以这样理解,这个下标其实是与lowcost相同位置的值对应的,是表示lowcost里这个权值是对于第几个顶点而言的最小值,adjvex里的这个值和lowcost里那个数据对应的下标,就是这个权值所代表的边的两个端点。

具体两个数组是何用处,可以在不断的演练上述过程中发现规律。

2.对于第二个小循换而言,为什么是找相对位置的小值。

也就是说如何理解代码 if (lowcost[j] != 0 && G.edge[k][j] < lowcost[j]),为什么是以k为顶点的每个权值去和对应地方的权值比较,而不是综合比较这个顶点的所有权值里面的最小值?

这个原因其实就是lowcost中所存数据的意义,是对于每个特定顶点的权值最小值,而不是所有的最小值。

0x06.普里姆算法感悟

普里姆算法就是巧妙的利用了两个数组,对每一次循环都能找到一个最小值去加入生成树。每一次循环都巧妙的找出了已加入生成树的顶点的相应边对应的权值。

巧妙利用数组的下标的意义,能够使原本复杂的代码简便不少。

对数组中的元素赋以特定的值使之含义丰富。

 

 

 

本章结束。

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(C数据结构与算法)