一个无向、加权、连通图的生成树是指这样一棵树,它包含该图所有的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; //~ ^_^ } |
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!
Be the first to comment on this entry.