最小生成樹——Prim演算法、Kruskal演算法和Boruvka演算法
最小生成樹
概述
實際上是最小權重生成樹的簡稱。在一給定的加權無向圖G = (V, E) 中,(u, v) 代表連線頂點 u 與頂點 v 的邊,而 w(u, v) 代表此邊的權重,若存在 T 中的頂點是所有V,T的邊是E的子集中,且T中沒有環,而且 w(T) 最小,則此 T 為 G 的最小生成樹。
簡單來講,最小生成樹(樹無環)包含了圖中所有的點,幷包含了能聯通全部頂點的最小權重的n-1條邊。
本文使用的尋找最小生成樹的三個演算法,Prim演算法、Kruskal演算法和Boruvka演算法,都是貪心演算法的應用。
性質
- 一個圖可以有多個生成樹,但是如果每條邊的權重都不同(distinct),則只有一個最小生成樹。
在每條邊權重都不同的情況下:
Cut property:如果S是V(G)的一個合適的非空子集,e(v, w)是一條連線S中的點v和不在S中的點w的一條權重最小的邊,v ∈ S 而且 w ∈/ S,那麼e(v, w)一定是某個(所有)最小生成樹中的一條邊。 這是Kruskal演算法的基礎
Cycle Property:如果C是圖G中的一個環,那麼C中具有最大權重的邊e一定不屬於某個(所有)最小生成樹
如果在最小生成樹中新增一條邊,則肯定會構成環。這與Cycle property一起成為證明生成樹的定理。
實際應用
最小生成樹的目的是建立最經濟的聯通子圖。可以有以下應用:
1. 城市之間的交通系統
2. 石油管道規劃
Prim演算法
1.概覽
普里姆演算法(Prim演算法),圖論中的一種演算法,可在加權連通圖裡搜尋最小生成樹。即由此演算法搜尋到的邊子集所構成的樹中,不但包括了連通圖裡的所有頂點(Vertex or Node),且其所有邊的權值之和最小。該演算法於1930年由捷克數學家沃伊捷赫·亞爾尼克(英語:Vojtěch Jarník)發現;並在1957年由美國電腦科學家羅伯特·普里姆(英語:Robert C. Prim)獨立發現;1959年,艾茲格·迪科斯徹再次發現了該演算法。因此,在某些場合,普里姆演算法又被稱為DJP演算法、亞爾尼克演算法或普里姆-亞爾尼克演算法。
2.演算法簡單描述
1).輸入:一個加權連通圖G (V, E),其中頂點集合為V,邊集合為E;一系列非負邊權重值 w(u, v)。
2).初始化:Vnew = {x},其中x為集合V中的任一節點(演算法起始點),Enew = {},為空;
3).重複下列操作,直到Vnew = V:
a.在集合E中選取權值最小的邊(u, v),其中u為集合Vnew中的元素,即已經被選擇的點,而v不在Vnew集合當中,即與被選擇的點相連的未被選擇的點,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
b.將v加入集合Vnew中,將(u, v)邊加入集合Enew中;
4).輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。
圖例 | 說明 | 不可選 | 可選 | 已選(Vnew) |
---|---|---|---|---|
此為原始的加權連通圖。每條邊一側的數字代表其權值。 | - | - | - | |
頂點D被任意選為起始點。頂點A、B、E和F通過單條邊與D相連。A是距離D最近的頂點,因此將A及對應邊AD以高亮表示。 | C, G | A, B, E, F | D | |
下一個頂點為距離D或A最近的頂點。B距D為9,距A為7,E為15,F為6。因此,F距D或A最近,因此將頂點F與相應邊DF以高亮表示。 | C, G | B, E, F | A, D | |
演算法繼續重複上面的步驟。距離A為7的頂點B被高亮表示。 | C | B, E, G | A, D, F | |
在當前情況下,可以在C、E與G間進行選擇。C距B為8,E距B為7,G距F為11。E最近,因此將頂點E與相應邊BE高亮表示。 | 無 | C, E, G | A, D, F, B | |
這裡,可供選擇的頂點只有C和G。C距E為5,G距E為9,故選取C,並與邊EC一同高亮表示。 | 無 | C, G | A, D, F, B, E | |
頂點G是唯一剩下的頂點,它距F為11,距E為9,E最近,故高亮表示G及相應邊EG。 | 無 | G | A, D, F, B, E, C | |
現在,所有頂點均已被選取,圖中綠色部分即為連通圖的最小生成樹。在此例中,最小生成樹的權值之和為39。 | 無 | 無 | A, D, F, B, E, C, G |
3.簡單證明prim演算法
反證法:假設prim生成的不是最小生成樹
1).設prim生成的樹為T
2).假設存在Gmin使得cost(Gmin) < cost(T) 則在Gmin中存在(u, v)不屬於T
3).將(u, v)加入T中可得一個環,且(u, v)不是該環的最長邊(這是因為(u, v)∈Gmin)
4).這與prim每次生成最短邊矛盾
5).故假設不成立,命題得證.
4.演算法程式碼實現(未檢驗)
#define MAX 100000
#define VNUM 10+1 //這裡沒有ID為0的點,so id號範圍1~10
int edge[VNUM][VNUM]={/*輸入的鄰接矩陣*/};
int lowcost[VNUM]={0}; //記錄Vnew中每個點到V中鄰接點的最短邊
int addvnew[VNUM]; //標記某點是否加入Vnew
int adjecent[VNUM]={0}; //記錄V中與Vnew最鄰近的點
void prim(int start)
{
int sumweight=0;
int i,j,k=0;
for(i=1;i<VNUM;i++) //頂點是從1開始
{
lowcost[i]=edge[start][i];
addvnew[i]=-1; //將所有點至於Vnew之外,V之內,這裡只要對應的為-1,就表示在Vnew之外
}
addvnew[start]=0; //將起始點start加入Vnew
adjecent[start]=start;
for(i=1;i<VNUM-1;i++)
{
int min=MAX;
int v=-1;
for(j=1;j<VNUM;j++)
{
if(addvnew[j]!=-1&&lowcost[j]<min) //在Vnew之外尋找最短路徑
{
min=lowcost[j];
v=j;
}
}
if(v!=-1)
{
printf("%d %d %d\n",adjecent[v],v,lowcost[v]);
addvnew[v]=0; //將v加Vnew中
sumweight+=lowcost[v]; //計算路徑長度之和
for(j=1;j<VNUM;j++)
{
if(addvnew[j]==-1&&edge[v][j]<lowcost[j])
{
lowcost[j]=edge[v][j]; //此時v點加入Vnew 需要更新lowcost
adjecent[j]=v;
}
}
}
}
printf("the minmum weight is %d",sumweight);
}
5.時間複雜度
這裡記頂點數v,邊數e
鄰接矩陣:O(v2) 鄰接表:O(elog2v)
Kruskal演算法
1.概覽
Kruskal演算法是一種用來尋找最小生成樹的演算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有前面Prim演算法和Boruvka演算法等。和Boruvka演算法不同的地方是,Kruskal演算法在圖中存在相同權值的邊時也有效。
2.演算法簡單描述
1).輸入:一個加權連通圖G (V, E),其中頂點集合為V,邊集合為E;一系列非負邊權重值 w(u, v)。
2).初始化:新建圖G’,G’中擁有原圖中相同的全部e個頂點,但沒有邊。
3).將原圖Graph中所有e個邊按權值從小到大排序
4).迴圈:從權值最小的邊開始遍歷每條邊 直至圖Graph中所有的節點都在同一個連通分量中
if 這條邊連線的兩個節點於圖Graphnew中不在同一個連通分量中
新增這條邊到圖Graphnew中
實際上進行的就是環的判斷,如果新增新的邊進入圖中會產生環,則跳過這條邊。
圖例描述:
圖例 | 說明 | 已選(Vnew) |
---|---|---|
首先第一步,我們有一張圖Graph,有若干點和邊 | 無 | |
將所有的邊的長度排序,用排序的結果作為我們選擇邊的依據。這裡再次體現了貪心演算法的思想。資源排序,對區域性最優的資源進行選擇,排序完成後,我們率先選擇了邊AD。這樣我們的圖就變成了左圖 | AD | |
在剩下的變中尋找。我們找到了CE。這裡邊的權重也是5 | AD,CE | |
依次類推我們找到了6,7,7,即DF,AB,BE。 | AD,CE,DF,AB,BE | |
下面繼續選擇, BC或者EF儘管現在長度為8的邊是最小的未選擇的邊。但是現在他們已經連通了(對於BC可以通過CE,EB來連線,類似的EF可以通過EB,BA,AD,DF來接連)。所以不需要選擇他們。類似的BD也已經連通了(這裡上圖的連通線用紅色表示了)。最後就剩下EG和FG了。當然我們選擇了EG。最後成功的圖就是左 | AD,CE,DF,AB,BE,EG |
3.簡單證明Kruskal演算法
對圖的頂點數n做歸納,證明Kruskal演算法對任意n階圖適用。
歸納基礎:
n=1,顯然能夠找到最小生成樹。
歸納過程:
假設Kruskal演算法對n≤k階圖適用,那麼,在k+1階圖G中,我們把最短邊的兩個端點a和b做一個合併操作,即把u與v合為一個點v’,把原來接在u和v的邊都接到v’上去,這樣就能夠得到一個k階圖G’(u,v的合併是k+1少一條邊),G’最小生成樹T’可以用Kruskal演算法得到。
我們證明T’+{(u,v)}是G的最小生成樹。
用反證法,如果T’+{(u,v)}不是最小生成樹,最小生成樹是T,即W(T) < W(T’+{(u,v)})。顯然T應該包含(u,v),否則,可以用(u,v)加入到T中,形成一個環,刪除環上原有的任意一條邊,形成一棵更小權值的生成樹。而T-{(u,v)},是G’的生成樹。所以W(T-{(u,v)})<=W(T’),也就是W(T)<=W(T’)+W((u,v))=W(T’+{(u,v)}),產生了矛盾。於是假設不成立,T’+{(u,v)}是G的最小生成樹,Kruskal演算法對k+1階圖也適用。
由數學歸納法,Kruskal演算法得證。
4.程式碼演算法實現
typedef struct
{
char vertex[VertexNum]; //頂點表
int edges[VertexNum][VertexNum]; //鄰接矩陣,可看做邊表
int n,e; //圖中當前的頂點數和邊數
}MGraph;
typedef struct node
{
int u; //邊的起始頂點
int v; //邊的終止頂點
int w; //邊的權值
}Edge;
void kruskal(MGraph G)
{
int i,j,u1,v1,sn1,sn2,k;
int vset[VertexNum]; //輔助陣列,判定兩個頂點是否連通
int E[EdgeNum]; //存放所有的邊
k=0; //E陣列的下標從0開始
for (i=0;i<G.n;i++)
{
for (j=0;j<G.n;j++)
{
if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)
{
E[k].u=i;
E[k].v=j;
E[k].w=G.edges[i][j];
k++;
}
}
}
heapsort(E,k,sizeof(E[0])); //堆排序,按權值從小到大排列
for (i=0;i<G.n;i++) //初始化輔助陣列
{
vset[i]=i;
}
k=1; //生成的邊數,最後要剛好為總邊數
j=0; //E中的下標
while (k<G.n)
{
sn1=vset[E[j].u];
sn2=vset[E[j].v]; //得到兩頂點屬於的集合編號
if (sn1!=sn2) //不在同一集合編號內的話,把邊加入最小生成樹
{
printf("%d ---> %d, %d",E[j].u,E[j].v,E[j].w);
k++;
for (i=0;i<G.n;i++)
{
if (vset[i]==sn2)
{
vset[i]=sn1;
}
}
}
j++;
}
}
5.時間複雜度
O(n) = elog2e //e為圖中的邊數
Boruvka演算法
1.概述
Brouvka演算法又名Sollin演算法。是最小生成樹最古老的一個演算法之一,其實是Prim演算法和Kruskal演算法的綜合,每次迭代同時擴充套件多課子樹,直到得到最小生成樹T。適用於並行處理一個圖。
2.演算法簡單描述**
1.用定點陣列記錄每個子樹(一開始是單個定點)的最近鄰居。(類似Prim演算法)
2.對於每一條邊進行處理(類似Kruskal演算法)
如果這條邊連成的兩個頂點同屬於一個集合,則不處理,否則檢測這條邊連線的兩個子樹,如果是連線這兩個子樹的最小邊,則更新(合併)
由於每次迴圈迭代時,每棵樹都會合併成一棵較大的子樹,因此每次迴圈迭代都會使子樹的數量至少減少一半,或者說第i次迭代每個分量大小至少為。所以,迴圈迭代的總次數為O(logn)。每次迴圈迭代所需要的計算時間:對於第2步,每次檢查所有邊O(m),去更新每個連通分量的最小弧;對於第3步,合併個子樹。所以總的複雜度為O(E*logV)。