最小生成树

来源:互联网 发布:剪辑声音的软件 编辑:程序博客网 时间:2024/06/05 11:22

转自:酷~行天下http://mindlee.net/2011/11/16/minimum-spanning-trees/

 假设要在 n 个城市之间建立通讯联络网,则连通 n 个城市只需要修建 n-1条线路,如何在最节省经费的前提下建立这个通讯网?答案就是:最小生成树。术语描述是:在 e条带权的边中选取 n-1 条边(不构成回路),使“权值之和”为最小。

     如右图是一个无向连通图,图中显示各条边的权值,带阴影的边为最小生成树的边,树中各边的权值之和为37。最小生成树不是唯一的:如图,用边(a, h)替代边(b,c)得到的是另外一棵最小生成书,其中各边权值之和也是37。如果还是不够清楚,再上一张图:(如下,借用自lcy课件),将最小生成树单独分离画出来,明显多了。

     解决最小生成树问题有两种算法:Kruskal算法和Prim算法。在了解这两种算法之前,先得了解一下MST性质(最小生成树性质):设G = (V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。通俗的讲,就是最小权值的边必定在最小生成树上,前提是,边的一个顶点在生成树上,另一个点不在。

一、Kruskal算法

     考虑问题的出发点: 为使生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能地小。具体做法: 先构造一个只含 n 个顶点的子图 SG(Sub-Graph),然后从权值最小的边开始,若它的添加不使SG 中产生回路,则在 SG 上加上这条边,如此重复,直至加上 n-1 条边为止。看图示应该会更清楚:

     简单的说,Kruskal算法过程就是,每次寻找最小权值的边,然后加入到最小生成树中,这个过程用到的操作,就是并查集,FIND-SET(u)返回最小包含u的集合中的一个代表元素(最小权值边的顶点),通过测试FIND-SET(u)是否等价于FIND-SET(v)判定顶点u和v是否属于同一棵树。通过过程UNION,实现树与树的合并。简单伪代码:

MST-KRUSKAL(G, w)
1  A ← Ø
2  for each vertex v ∈ V[G]
3       do MAKE-SET(v)
4  sort the edges of E into nondecreasing order by weight w
5  for each edge (u, v) ∈ E, taken in nondecreasing order by weight
6       do if FIND-SET(u) ≠ FIND-SET(v)
7             then A ← A ∪ {(u, v)}
8                  UNION(u, v)
9  return A

     第1~3行将集合A初始化为空集,并建立|V|棵树,每棵树都包含了图的一个顶点。在第4行中,根据权值的非递减性顺序,对E中的边进行排序。在第5~8行的for循环中,首先检查每条边(u, v),其端点u和v是否属于同一棵树。如果是,把(u,v)加入到森林中就会形成一个回路,所以放弃边(u, v),否则说明两个顶点分属于不同的树,由第7行把边加入集合A中,第8行对两棵树中的顶点进行合并。

     Kruskal的代码实现,可以从一道题目开始(HDU 1233 还是畅通工程),这个题目的大意,和本文开始时描述的情形类似。这个题目可以用并查集 + Kruskal + Prim三种方法解决。下面是Kruskal方法解决代码(刚整理的Kruskal模板)

[cpp] view plaincopy
  1. #include<iostream>  
  2. #include<cstdio>  
  3. #include<cstring>  
  4. #include<cmath>  
  5. #include<algorithm>  
  6. using namespace std;  
  7.    
  8. const int NV = 101;  
  9. const int NE = 5011;  
  10.    
  11. struct Kruskal {  
  12.     int n, size;  
  13.     int root[NV];  
  14.     int mst;  
  15.    
  16.     struct Edge {  
  17.         int u, v, w;  
  18.         Edge () {}  
  19.         Edge (int U, int V, int W = 0) : u(U), v(V), w(W) {}  
  20.         bool operator < (const Edge &rhs) const {//从小到大  
  21.             return w < rhs.w;  
  22.         }  
  23.     } E[NE];  
  24.    
  25.     inline void init(int x) {  
  26.         n = x, size = 0;  
  27.         for (int i = 0; i < x; i++) {  
  28.             root[i] = i;  
  29.         }  
  30.     }  
  31.    
  32.     inline void insert(int u, int v, int w = 0) {  
  33.         E[size++] = Edge(u, v, w);  
  34.     }  
  35.    
  36.     int Getroot (int n) {  
  37.         int r = n;  
  38.         while (r != root[r]) r = root[r];  
  39.         int x = n, y;  
  40.         while (x != r) {  
  41.             y = root[x];  
  42.             root[x] = r;  
  43.             x = y;  
  44.         }  
  45.         return r;  
  46.     }  
  47.    
  48.     bool Union(int x, int y) {  
  49.         int fx = Getroot(x), fy = Getroot(y);  
  50.         if (fx != fy) {  
  51.             root[fx] = fy;  
  52.             return true;  
  53.         }  
  54.         return false;  
  55.     }   
  56.    
  57.     int kruskal () {  
  58.         mst = 0;  
  59.         sort(E, E + size);  
  60.         int cnt = 0;  
  61.         for (int i = 0; i < size; i++) {  
  62.             if (Union(E[i].u, E[i].v)) {  
  63.                 mst += E[i].w;  
  64.                 if (++cnt == n - 1) break// 优化  
  65.             }  
  66.         }  
  67.         return mst;  
  68.     } // kruskal O(E)  
  69. }G;  
  70.    
  71. int main() {  
  72.     int n;  
  73.     while (~scanf("%d", &n), n) {  
  74.         G.init(n);  
  75.         int k = n * (n - 1) / 2;  
  76.         for (int i = 0; i < k; i++) {  
  77.             int a, b, c;  
  78.             scanf("%d%d%d", &a, &b, &c);  
  79.             G.insert(a - 1, b - 1, c);  
  80.         }  
  81.         printf("%d\n", G.kruskal());  
  82.     }  
  83.     return 0;  
  84. }  

二、Prim算法

     取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 u。在添加的顶点 u 和已经在生成树上的顶点v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 u 之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有 n-1 个顶点为止。也就是,每次添加到树中的边,都是树的权尽可能小的边。图示:

     实现过程,即每次找到和它连接所有边中最小权值的边(寻找最小值,可以用最小堆,斐波那契堆等),然后加到最小生成树中即可,伪代码:

MST-PRIM(G, w, r)
1  for each u ∈ V [G]
2       do key[u] ← ∞
3          π[u] ← NIL
4  key[r] ← 0
5   Q ← V [G]
6   while Q ≠ Ø
7       do u ← EXTRACT-MIN(Q)
8          for each v ∈ Adj[u]
9              do if v ∈ Q and w(u, v) < key[v]
10                    then π[v] ← u
11                         key[v] ← w(u, v)

     1~5行置每个顶点的域为inf(根顶点r除外,r的key域被置为0,这样它就会成为第一个被处理的顶点),6~11更新权值信息。同样是上边那道题目,Prim算法实现:

[cpp] view plaincopy
  1. #include<iostream>  
  2. #include<cstdio>  
  3. #include<cstring>  
  4. #include<cmath>  
  5. #include<algorithm>  
  6. using namespace std;  
  7. const int NV = 101;  
  8. const int inf = 0x7f7f7f7f;  
  9.    
  10. int n, m, a, b, w, gra[NV][NV];  
  11. bool mark[NV];  
  12. //weight权值,pre记录边的前一个点  
  13. int weight[NV], pre[NV];  
  14.    
  15. int prim (int src) {  
  16.     int  id;  
  17.     int ans = 0;  
  18.     memset(mark,false,sizeof(mark));  
  19.     for (int i = 1; i <= n; i++) {  
  20.         weight[i] = gra[src][i];  
  21.         pre[i] = src;  
  22.     }  
  23.     mark[src] = true;  
  24.     //n个节点至少需要n - 1条边构成最小生成树  
  25.     for (int i = 1; i < n; i++) {  
  26.         id = -1;  
  27.         for (int j = 1; j <= n; j++) {  
  28.             if (mark[j] ) continue;  
  29.             ///权值较小且不在生成树中  
  30.             if (id == -1 || weight[j] < weight[id]) {  
  31.                 id = j;  
  32.             }  
  33.         }  
  34.         if (gra[ pre[id] ][ id ] == inf) return -1;  
  35.         ans += gra[ pre[id] ][ id ];  
  36.         mark[id] = true;  
  37.         //更新当前节点到其他节点的权值, 更新权值信息  
  38.         for (int j = 1; j <= n; j++) {  
  39.             if (mark[j]) continue;  
  40.             if (weight[j] > gra[id][j]) {  
  41.                 weight[j] = gra[id][j];  
  42.                 pre[j] = id;  
  43.             }//if  
  44.         }  
  45.     }//for  
  46.     return ans;  
  47. }  
  48.    
  49. int main() {  
  50.     while(~scanf("%d", &n), n) {  
  51.         for (int i = 1; i <= n; i++) {  
  52.             gra[i][i] = 0;  
  53.             for (int j = i + 1; j <= n; j++) {  
  54.                 gra[i][j] = inf;  
  55.             }  
  56.         }//for  
  57.         m = n * (n - 1) / 2;  
  58.         while (m--) {  
  59.             scanf("%d%d%d", &a, &b, &w);  
  60.             gra[a][b] = gra[b][a] = w;  
  61.         }  
  62.         printf("%d\n",prim(1));  
  63.     }  
  64.     return 0;  
  65. }  

三、Kruskal和Prim对比

1)过程简单对比

Kruskal算法:所有的顶点放那,每次从所有的边中找一条代价最小的;

Prim:算法:在U,(V – U)之间的边,每次找一条代价最小的

2)效率对比

效率上,稠密图 Prim > Kruskal;稀疏图 Kruskal > Prim

Prim适合稠密图,因此通常使用邻接矩阵储存,而Kruskal多用邻接表。

3)空间对比

     解决最小生成树题目的时候,要根据给出数据的情况,来决定使用哪种算法,比如:1)当点少边多的时候,如1000个点500000条边,这样BT的数据,用prim做就要开一个1000 * 1000的二维数组,而用kruskal做只须开一个500000的数组,500000跟1000*1000比较相差一半。2)当点多边少的时候,如1000000个点2000条边,像这种数据就是为卡内存而存在的,如果用prim做,你想开一个1000000 * 1000000的二维数组,内存绝对爆掉,而用kruskal只须开一个2000的数组就可以了。

原创粉丝点击