文章出自:http://dsqiu.iteye.com/blog/1689178
最小生成樹——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;
- }
- }
- }
小結
這篇文章詳細的講解了圖最小生成樹的三個算法的原理和實現,希望能派上用場。如果你有任何建議或者批評和補充,請留言指出,不勝感激,更多參考請移步互聯網。