最小生成树—Prim算法

我们要讨论的问题是如何在一个无向图中找到它的最小生成树,虽然这个问题对有向图也有意义,但是处理起来更麻烦。

一个无向图 G 的最小生成树就是连接 G 上所有顶点的边构成的树,且这些边的总权值最低。当且仅当图是连通的才有最小生成树。

在无向图的最小生成树中,边的条数为|V|-1。最小生成树是一棵树,因为它无圈;边的总权值最低,所以它最小;包含所有顶点,所以是生成树。显然,最小生成树是包含所有顶点的最小的树。如果我们需要给一所房子里安装电路,那么这就是一个最小生成树问题。

对于任意生成树,如果向树中增加一条不属于树的边e,就会形成一个圈,除去圈中任意一条边,又会变回生成树。如果边e的值比除去的边值低,那么新的生成树的值就比原树小。如果在建立生成树时所添加的边在所有不成圈的边值中最小,那么最后得到的生成树就不能被改进,否则更改后的树要么成圈,要么权值更高。

最小生成树的建立方法有两种:Prim算法和Kruskal算法。下面以下图为例,从v_{1}出发,建立该图的最小生成树。

最小生成树—Prim算法_第1张图片

Prim算法:

计算最小生成树的方法是使其连续的一步步的成长。在每一步,都要把当前节点当作根节点并往上加边。 

Prim算法与Djkstra算法非常相似(Djkstra算法在前一篇文章),在处理当前顶点v时,先将其标记为已知,如果v的邻接顶点w未知,且对应的dvw要小于w当前的d,那么就需要更新w顶点的d以及path,与Djkstra算法不同的是,d表示的不再是从w到输入顶点s的距离,而是w与邻接顶点v的距离,所以其是否更新的判断法则也需要改变。这个算法也是一种贪婪算法,因为在每一步,我们所加入的都是目前权值最小的边,这个算法不会形成圈,因为在算法中不会再改变已知的顶点,并且每一步都将处理顶点视为根节点,所以任何顶点都只能与最后使其d改变的顶点直接相连。

下面说明Prim算法的具体过程,从v_{1}开始建立最小生成树:

最小生成树—Prim算法_第2张图片

v Known d path
v_{1} 0 0 -1
v_{2} 0 inf -1
v_3 0 inf -1
v_4 0 inf -1
v_5 0 inf -1
v_6 0 inf -1
v_7 0 inf -1

 与Djkstra算法一样,首先找到d最小的未知顶点,即v_1,将其标记为已知,并更新与它邻接的未知顶点的信息,并且当前更新边中的最小边添加进树(这个操作实际上就是标记该节点为已知,则该节点和其path节点之间的边就被添加进树中,加上边(v_1,v_1)(假设有),总共七条边添加在树中,也就是所有的顶点都被标记为已知,算法就结束):

最小生成树—Prim算法_第3张图片

v Known d path
v_{1} 1 0 -1
v_{2} 0 2 v_{1}
v_3 0 4 v_{1}
v_4 0 1 v_{1}
v_5 0 inf -1
v_6 0 inf -1
v_7 0 inf -1

接下来,d最小的未知顶点是v_4,将其标记为已知,并更新其邻接顶点信息,由于v_1已知,所以不考察v_1,而边(v_4,v_2)的值要大于目前v_2的d,所以v_2的信息不变,但边(v_4,v_3)的值小于v_3目前的d,所以v_3的信息要进行更新,而目前最小的边为(v_1,v_2),所以将它添加进树:

最小生成树—Prim算法_第4张图片

v Known d path
v_{1} 1 0 -1
v_{2} 0 2 v_{1}
v_3 0 2 v_4
v_4 1 1 v_{1}
v_5 0 7 v_4
v_6 0 8 v_4
v_7 0 4 v_4

 下一个要选择的顶点是v_{2},并加入目前的最小边,显然它不会使任何信息改变(除过其自身的Known变为1)。

最小生成树—Prim算法_第5张图片

v Known d path
v_{1} 1 0 -1
v_{2} 1 2 v_{1}
v_3 0 2 v_4
v_4 1 1 v_{1}
v_5 0 7 v_4
v_6 0 8 v_4
v_7 0 4 v_4

下面要选择的顶点是v_3,对其进行与之前顶点同样的处理:

最小生成树—Prim算法_第6张图片

v Known d path
v_{1} 1 0 -1
v_{2} 1 2 v_{1}
v_3 1 2 v_4
v_4 1 1 v_{1}
v_5 0 7 v_4
v_6 0 5 v_3
v_7 0 4 v_4

下一步选择的顶点是v_7

最小生成树—Prim算法_第7张图片

v Known d path
v_{1} 1 0 -1
v_{2} 1 2 v_{1}
v_3 1 2 v_4
v_4 1 1 v_{1}
v_5 0 6 v_7
v_6 0 1 v_7
v_7 1 4 v_4

接下来选择v_{6},并将最小的边加入树中,它不会对任何信息产生影响 :

最小生成树—Prim算法_第8张图片

v Known d path
v_{1} 1 0 -1
v_{2} 1 2 v_{1}
v_3 1 2 v_4
v_4 1 1 v_{1}
v_5 0 6 v_7
v_6 1 1 v_7
v_7 1 4 v_4

 最后考察v_{5},它也不会对任何信息产生影响:

最小生成树—Prim算法_第9张图片

v Known d path
v_{1} 1 0 -1
v_{2} 1 2 v_{1}
v_3 1 2 v_4
v_4 1 1 v_{1}
v_5 1 6 v_7
v_6 1 1 v_7
v_7 1 4 v_4

到此,就成功建立了该图的最小生成树,代码如下:

void Prim(g* p) {
	int min, i, Dmin;
	for (;;) {
		Dmin = inf;//找到d最小的未知顶点
		for (i = 0; i < 7; i++) {
			if (p->v[i]->known == 0 && p->v[i]->d < Dmin) {
				min = i;
				Dmin = p->v[i]->d;
			}
		}
		if (Dmin == inf) {//满足这个条件时,要么顶点全部已知,要么图是不连通的
			break;
		}
		p->v[min]->known = 1;
		l* tmp = p->v[min]->next;
		while (tmp != NULL) {
			if (p->v[tmp->val]->known == 0) {
				if (p->v[tmp->val]->d > tmp->dvw) {//判断条件相比Djkstra算法改变
					p->v[tmp->val]->d = tmp->dvw;
					p->v[tmp->val]->path = min;
				}
			}
			tmp = tmp->next;
		}
	}
}

使用两层循环的Prim算法时间复杂度为O(|V|^2),当图稠密时,使用这样的方法是好的,如果图是稀疏的,使用二叉堆的时间复杂度为O(|E|log|V|),对于稀疏图来说,这是一个好的界(当图是稠密的,则有|E|=O(|V|^2),所以使用两层循环是合适的,当图稀疏时,不再满足这个条件,那么 O(|E|log|V|)的界就比O(|V|^2)好很多了)。前面提到当且仅当图是连通的才具有最小生成树,判断图是否连通很简单,当程序中最外层的for循环终止后,如果图中仍存在顶点的d=inf,那么就说明图是不连通的。

你可能感兴趣的:(算法,数据结构,图论,c语言)