一个无向、加权、连通图的生成树是指这样一棵树,它包含该图所有的n个顶点,和该图边集E中的n-1个边。最小生成树就是该图所有生成树中各边权值之和(构造代价)最小的生成树。求指定图的最小生成树有多种算法,除了这里要介绍的Kruskal算法,还有Prim算法、Sollin算法等。

Kruskal算法描述

  用Kruskal算法为无向加权连通图G构造最小生成树T的步骤如下:首先初始化T为一个包含所有n个顶点、0个边的森林,E为G的边集。每次从E中取出一条具有最小权值的边(并从E中删除该边),如果该边不与T中其他边构成回路,则将该边加入T,否则舍弃该边。重复上述操作至T中含有n-1条边,此时T即Kruskal算法构造出的最小生成树。

Kruskal算法证明

  易证,对于一个无向加权连通图,总是存在一棵或以上的有限课生成树,而这些生成树中肯定存在至少一棵最小生成树。下面证明Kruskal算法构造的生成树是这些最小生成树中的一棵。
  设T为Kruskal算法构造出的生成树,U是G的最小生成树。如果T==U那么证明结束。如果T != U,我们就需要证明T和U的构造代价相同。由于T != U,所以一定存在k > 0条边存在于T中,却不在U中。接下来,我们做k次变换,每次从T中取出一条不在U中的边放入U,然后删除U一条不在T中的边,最后使T和U的边集相同。每次变换中,把T中的一条边e加入U,同时删除U中的一条边f。e、f按如下规则选取:a). e是在T中却不在U中的边中最小的一条边;b). e加入U后,肯定构成唯一的一个环路,令f是这个环路中的一条边,但不在T中。f一定存在,因为T中没有环路。

  这样的一次变换后,U仍然是一棵生成树。
  我们假设e权值小于f,这样变换后U的代价一定小于变换前U的代价,而这和我们之前假设U是最小生成树矛盾,因此e权值不小于f。
  再假设e权值大于f。由于f权值小于e,由Kruskal算法知,f在e之前从E中取出,但被舍弃了。一定是由于和权值小于等于f的边构成了环路。但是T中权值小于等于f(小于e)的边一定存在于U中,而f在U中却没有和它们构成环路,又推出矛盾。所以e权值不大于f。于是e权值等于f。
  这样,每次变换后U的代价都不变,所以K次变换后,U和T的边集相同,且代价相同,这样就证明了T也是最小生成树。由证明过程可以知道,最小生成树可以不是唯一的。

Kruskal算法的C++实现

  利用最小堆来存储边集E,利用并-查集来判断向T中添加边是否构成环路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
 
struct Edge 
{
	int from, to, w; //~ 不要被假象迷惑,这里是无向图
	Edge(int f, int t, int _w): from(f), to(t), w(_w){}
	/*
	//~ bool operator <(const Edge& e){ return w < e.w; }
	bool operator >(const Edge& e){ return w > e.w; }
	*/
};
//~ 为什么我把operator<重载为成员会出错?
//~ bool operator <(const Edge& e1, const Edge& e2){ return e1.w < e2.w; }
bool operator >(const Edge& e1, const Edge& e2){ return e1.w > e2.w; } 
bool AddEdge(vector<int> & V, const Edge& e);
int main(int argc, char* argv[])
{
	vector<Edge> E;
	int from, to, w;
	int n; //~ 顶点数
	cin>>n;
	vector<int> V(n+1, -1); //~ 顶点并查集
	while (cin>>from>>to>>w) E.push_back(Edge(from, to, w));
 
	make_heap(E.begin(), E.end(), greater<Edge>());
	int count = 0; //~ 已添加边数
	while (E.size())
	{
		Edge e = E[0];
		if(AddEdge(V, e)) //~ 将成功添加的边输出
		{
			count++;
			if(count == n - 1) break; //~ 树已生成完毕
			cout<<e.from<<"->"<<e.to<<": "<<e.w<<endl;
		}
		pop_heap(E.begin(), E.end(),greater<Edge>());
		E.pop_back();
	}
	if (count != n - 1) cout<<"I cannot do what you want."<<endl;
	return 0;
}
 
bool AddEdge(vector<int> & V, const Edge& e)
{
	int i = e.from;
	for (; V[i] > 0;) i = V[i]; //~ 寻找根节点
	int j = e.to;
	for (; V[j] > 0;) j = V[j]; //~ 寻找根节点
	if (i == j)	return false; //~ i,j两节点已经联通
	if (V[i] > V[j]) //~ 将小集合合并至大集合上
	{
		V[i] = j;
		V[j] += V[i];
	}
	else
	{
		V[j] = i;
		V[i] += V[j];
	}
	return true; //~ ^_^
}
Tags: ,.
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Home

Be the first to comment on this entry.

Name(required)
Mail (required),(will not be published)

RFC: Request For Comments. Orz..

Website(recommended)