最小生成树——Prim、Kruskal、Sollin(Boruvka)
本文内容框架:
1.Prim算法及其基于优先队列实现
2.Kruskal算法
3.Sollin算法
对于最小生成树,有两种算法可以解决。一种是Prim算法,该算法的时间复杂度为O(n²),与图中边数无关,该算法适合于稠密图,而另外一种是Kruskal,该算法的时间主要取决于边数,它较适合于稀疏图。
Prim算法
Prim算法描述
设图G =(V,E),其生成树的顶点集合为U。
①、把v0放入U。
②、在所有u∈U,v∈V-U的边(u,v)∈E中找一条最小权值的边,加入生成树。
③、把②找到的边的v加入U集合。如果U集合已有n个元素,则结束,否则继续执行②。
时间复杂度
最小边、权的数据结构 时间复杂度(总计)
邻接矩阵、搜索 | O(V2) |
二叉堆(后文伪代码中使用的数据结构)、邻接表 | O((V + E) log(V)) = O(E log(V)) |
斐波那契堆、邻接表 | O(E + V log(V)) |
(该图转自wikipedia)
通过邻接矩阵图表示的简易实现中,找到所有最小权边共需O(V²)的运行时间。使用简单的二叉堆与邻接表来表示的话,普里姆算法的运行时间则可缩减为O(E *log V),其中E为连通图的边数,V为顶点数。如果使用较为复杂的斐波那契堆,则可将运行时间进一步缩短为O(E + V* log V),这在连通图足够密集时(当E满足Ω(V* log V)条件时),可较显著地提高运行速度。
Prim算法实现
用优先队列实现
- #include <iostream>
- #include <cstdio>
- #include <cstdlib>
- #include <cstring>
- #include <cmath>
- #include <algorithm>
- #include <set>
- #include <map>
- #include <vector>
- #include <queue>
- #include <ctime>
- using namespace std;
- #define LL long long
- const int N = 2000;
- const int INF = 1 << 30;
- struct Node
- {
- int v,next,w;
- bool operator < (const Node &a) const
- {
- return w > a.w;
- }
- } p[N],t1,t2;
- int dis[N],vis[N],head[N],cnt;
- int res;
- void addedge(int u,int v,int w)
- {
- p[cnt].v = v;
- p[cnt].next = head[u];
- p[cnt].w = w;
- head[u] = cnt++;
- }
- void prim()
- {
- priority_queue<Node> q;
- for(int i = head[0] ; i != -1 ; i = p[i].next)
- {
- int v = p[i].v;
- if(p[i].w < dis[v])
- {
- dis[v] = p[i].w;
- t1.w = dis[v];
- t1.v = v;
- q.push(t1);
- }
- }
- vis[0] = 1;
- while(!q.empty())
- {
- t1 = q.top();
- q.pop();
- int u = t1.v;
- if(vis[u]) continue;
- vis[u] = 1;
- res += dis[u];
- for(int i = head[u]; i != -1; i = p[i].next)
- {
- int v = p[i].v;
- if(!vis[v] && dis[v] > p[i].w)
- {
- dis[v] = p[i].w;
- t2.v = v;
- t2.w = dis[v];
- q.push(t2);
- }
- }
- }
- }
- int main()
- {
- int n,m,w;
- while(scanf("%d",&n),n)
- {
- memset(p,0,sizeof(p));
- memset(head,-1,sizeof(head));
- memset(vis,0,sizeof(vis));
- char u,v;
- for(int i=0; i<n-1; i++)
- {
- cin>>u>>m;
- for(int j=0; j<m; j++)
- {
- cin>>v>>w;
- addedge(u-'A',v-'A',w);
- addedge(v-'A',u-'A',w);
- }
- }
- for(int i = 0 ; i < n ; i ++) dis[i] = INF;
- res = 0;
- prim();
- printf("%d\n",res);
- }
- return 0;
- }
转自http://blog.csdn.net/acceptedxukai/article/details/6978868
Kruskal算法
Kruskal算法与Prim算法的不同之处在于,Kruskal在找最小生成树结点之前,需要对所有权重边做从小到大排序。将排序好的权重边依次加入到最小生成树中,如果加入时产生回路就跳过这条边,加入下一条边。当所有结点都加入到最小生成树中之后,就找出了最小生成树。
Kruskal算法步骤
1.新建图G,G中拥有原图中相同的点,但没有边
2.将原图中所有的边按权值从小到大排序
3.从权值最小的边开始,如果这条边链接的两个点于图G中不在同一个连通分量中,则添加这条边到图G中
4.重复3,直至图G中所有的点都在同一个连通分量中
Kruskal算法实现
利用最小堆来存储边集E,利用并-查集来判断向T中添加边是否构成环路。
- #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;
- else
- V[j] = i;
- return true; //~ ^_^
转自http://www.cppblog.com/superKiki/archive/2010/05/02/114180.aspx
Sollin(Boruvka)算法
Sollin(Brouvka)算法虽然是最小生成树最古老的一个算法之一,其实是前面介绍两种算法的综合,每次迭代同时扩展多课子树,直到得到最小生成树T。
Sollin(Boruvka)算法步骤
1.用定点数组记录每个子树(一开始是单个定点)的最近邻居。(类似Prim算法)
2.对于每一条边进行处理(类似Kruskal算法)
如果这条边连成的两个顶点同属于一个集合,则不处理,否则检测这条边连接的两个子树,如果是连接这两个子树的最小边,则更新(合并)
由于每次循环迭代时,每棵树都会合并成一棵较大的子树,因此每次循环迭代都会使子树的数量至少减少一半,或者说第i次迭代每个分量大小至少为。所以,循环迭代的总次数为O(logn)。每次循环迭代所需要的计算时间:对于第2步,每次检查所有边O(m),去更新每个连通分量的最小弧;对于第3步,合并个子树。所以总的复杂度为O(E*logV)。
Sollin(Boruvka)算法实现
- typedef struct{int v;int w;double wt;}Edge;
- typeder struct{int V;int E;double **adj}Graph;
- /*nn存储每个分量的最邻近,a存储尚未删除且还没在MST中的边
- *h用于访问要检查的下一条边
- *N用于存放下一步所保存的边
- *每一步都对应着检查剩余的边,连接不同分量的顶点的边被保留在下一步中
- *最后每一步将每个分量与它最邻近的分量合并,并将最近邻边添加到MST中
- */
- Edge nn[maxE],a[maxE];
- void Boruvka(Graph g,Edge mst[])
- {
- int h,i,j,k,v,w,N;
- Edge e;
- int E=GRAPHedges(a,G);
- for(UFinit(G->V);E!=0;E=N)
- {
- for(k=0;k<G->V;k++)
- nn[k]=Edge(G->V,G->V,maxWT);
- for(h=0,N=0;h<E;h++)
- {
- i=find(a[h].v);j=find(a[h].w);
- if(i==h) continue;
- if(a[h].wt<nn[i].wt)nn[i]=a[h];
- if(a[h].wt<nn[j].wt)nn[j]=a[h];
- a[N++]=a[h];
- }
- for(k=0;k<G->V;k++)
- {
- e=nn[k];v=e.v;w=e.w;
- if(v!=G->V&&!UFfind(v,w))
- {
- UFunion(v,w);mst[k]=e;
- }
- }
- }
转自 http://dsqiu.iteye.com/blog/1689178