資料結構與演算法19-圖的最小生成樹
我們把構造連通網的最小代價生成樹稱為最小生成樹(Minimum Cost Spanning Tree)
普里姆(Prim)演算法
也就是說,現在我們已經有一個儲存結構
MGraph的G,它的arc二維陣列如圖所示。陣列中我們用65535代表∞
於是普里母演算法程式碼如下。其中INFINITY權值極大值,這裡用65535代表,MAXVEX為頂點個數最大值,此處大於等於9即可。分析MiniSpanTree_Prim函式,輸入上圖中的矩陣後,看看它是如何執行並打印出最小生成樹的。
/*Prim 演算法生成最小生成樹*/ void MiniSpanTree_Prim(MGraph G) { int min,i,j,k; int adjvex[MAXVEX]; //儲存相關頂點下標 int lowcost[MAXVEX];//儲存相關頂點間邊的權值 lowcost[0] = 0; //初始化第一個權值為0,即v0加入生成樹 //lowcost的值為0,在這裡就是此下標的頂點已加入生成樹 adjvex[0]=0; //初始化第一個頂點下標為0 for(i=1;i<G.numVertexes;i++){ { lowcost[i]=G.arc[0][i]; //將v0頂點與之有邊的權值存入陣列。 adjvex[i]=0; //初始化都為v0的下標 } for(i=1;i<G.numVertexes;i++) { min = INFINTY; //初始化最小權值為∞ j=1;k=0; while(j<G.numVertexes) { //迴圈全部頂點 if(lowcost[j]!=0 &&lowcost[j]<min) { min =lowcost[i]; //讓當前權值成為最小值 k=j; //將當前最小值的下標存入k } j++; } printf(“%d,%d”,adjvex[k],k); //列印當前頂點邊中權值最小的邊 lowcost[k] = 0; //將當前頂點權值 設定成0表示此頂點已完成任務 for(j=1;j<G.numVertexes;j++) { if(lowcost[j]!=0 && G.arc[k][j]<lowcost[j]) lowcost[j] =G.arc[k][j]; adjvex[j] = k; } } }
1. 程式開始執行,我們前4~6行,建立了兩個一維陣列lowcost和adjvex,長度都為頂點個數9。
2. 接下來分別給這兩個陣列的第一個下標位賦值為0,adjvex[0]=0其實意思就是我們現在從v0開始(事實上,最小生成樹從哪個頂點開始計算都無所謂,我們假定從v0開始),lowcost[0]=0表示v0已經被納入到最小生成樹中,之後凡是lowcost陣列中的值為0就是表示此下標的頂點被納入最小生成樹。
3. 接下來的迴圈我們讀取鄰接矩陣的第一行資料。將數值賦值給lowcost陣列,所以此時lowcost陣列為:{0,10,65535, 65535, 65535,11, 65535, 65535, 65535},而arjvex則全部為0。此時我們已經完成整個初始化工作,準備開始生成。
4. 接下來的迴圈過程就是構造最小生成樹過程
5. 將min值設定為65535,它的目的是為了之後找到一定範圍內的最小權值。j是用來做頂點下標迴圈的變數,k是用來儲存最小權值的頂點下標
6. 迴圈中不斷修改min為當前lowcost陣列中最小的值,並用k保留此最小值的頂點下標。經過迴圈後,min=10,k=1;注意這裡有個if判斷lowcost[j]!=0表示已經是生成樹的頂點不參與最小權值的查詢。
7. 列印k=1,adjvex[1]=0,所以結果為(0 1),表示v0至v1邊為最小生成樹第一條邊。如圖
8. lowcost[k] = 0;這句話的意思是目前k=1我們將lowcost[k]=0就是說頂點v1納入到最小生成樹中。此時lowcost陣列值為{0,0,65535, 65535, 65535,11, 65535, 65535, 65535}
9. 接下來的迴圈,j迴圈由1至8,因k=1,查詢鄰接矩陣的第v1行的各個可權值,與lowcost的對應值比較,若更小則修改lowcost值,並將k值存入adjvex陣列中。因第v1行有18,16,12均比65535小,所以最終lowcost陣列的值為
{0,0,65535, 65535,11,16, 65535,12}
adjvex陣列的值為:
{0,0,1,0,0,0,1,0,1}。這裡的if判斷lowcost[j]!=0也說明v0和v1已經是生成樹的頂點不參與最小權值的比對了。
10. 再次迴圈,由第15行到第26行,此時min=11,k=5,adjvex[5]=0。因此列印結構為(0,5)。表示v0至v5邊為最小生成樹的第二邊,如下圖
11. 接下來執行到36行,lowcost陣列的值為:{0,0,18,65535,26,0,16,65535,12}。adjvex陣列的值為{0,0,1,0,5,0,1,0,1}
12. 之後,依次模擬了。通過不斷的轉換,構造的過程如圖
有了上面的推演,普里姆(Prim)演算法的實現定義可能就容易理解一些
假設N=(V,{E})是連通圖,TE是N上最小生成樹中邊的集合。演算法從U={U0}(U0∈V),TE={}開始。重複執行下述操作:在所有u∈U,v∈V-U的邊(u,V)∈E中找一條代價最小的邊(u0,v0)併入集合TE,同時V0併入U,直至U=V為止。此時TE中必須n-1條邊,則T={V,{TE}}為N的最小生成樹。
由演算法程式碼中的迴圈巢狀可得知此演算法的時間複雜度為O(n2)
克魯斯卡爾演算法
我們可以直接就以邊為目標去構建,因為權值是在邊上,直接去找最小權值的邊來構建生成樹也是很自然的想法,只不過構建時要考慮是否會形成環路而已。此時我們就用到了圖的儲存結構中的邊集陣列結構。
程式碼如下:
typedef struct
{
int begin;
int end;
int weight;
}Edge;
我們的鄰接矩陣通過程式轉化如下圖右圖的邊集陣列,並且它們按權值從小到大排序。
於是克魯斯卡爾(Kruskal)演算法程式碼如下,其中MAXEDGE為邊數量的極大值,此處大於等於15即可。MAXVEX為頂點個數最大值,此處大於等於9即可。接下來我們模擬一下MiniSpanTree_Kruskal函式
void MiniSpanTree_Kruskal(MGraph G)
{
int i,n,m;
Edge edges[MAXEDGE];//定義邊集陣列
int parent[MAXVEX]; //定義一陣列來判斷邊與邊是否形成環路
/*此處省略將鄰接矩陣G轉化為邊集陣列edges並按權由小到大排序的程式碼*/
for(i=0;i<G.numVertexes;i++)
parent[i] = 0;
for(i=0;i<G.numEdges;i++)
{
n=Find(parent,edges[i].begin);
m=Find(parent,edges[i].end);
if(n!=m)
{
parent[n] = m;
print(“(%d,%d) %d”,edges[i].begin,edges[i].end,edges[i].weight)
}
}
}
int Find(int *parent,int f)
{
while(parent[f]>0)
f=parent[f];
return f;
}
1. 宣告陣列parent,並將它的值都初始化為0,它的作用我們後面慢慢說。
2. 我們開始對邊集陣列做迴圈遍歷,開始時,i=0。
3. 第10行我呼叫了Find函式,傳入的引數是陣列parent和當前權值最小邊(v4,v7)的begin:4。因為parent中全都是0,所以傳出的值使得n=4。
4. 第11行,同樣作法,傳入(v4,v7)的end:7。傳出值使得m=7。
5. 第12~16行,很顯然n與m不等,因此parent[4]=7。此時parent陣列值為
{0,0,0,0,7,0,0,0,0},並且列印得到“(4,7)7”。們已經將邊(v4,v7)納入到最小生成樹中,
6. 迴圈返回,執行10~16行,此時i=1,edge[1]得到邊(v2,v8),n=2,m=8,parent[2]=8,列印結果為“(2,8)8”,此時parent陣列為{0,0,8,0,7,0,0,0,0}此時parent陣列值為{0,0,8,0,7,0,0,0,0},這也就表示邊(v4,v7)和(v2,v8)已經納入到最小生成樹如下圖。
7. 再次執行10~16行,此時i=2,edge[2]得到邊(v0,v1),n=0,m=1,parent[0]=1,列印結果為“(0,1)10”,此時parent陣列值為{1,0,8,0,7,0,0,0,0},此時邊(v4,v7)、(v2,v8)和(v0,v1)已經納入到最小生成樹。
8. 當i=3、4、5、6、時,分別將邊(v0,v5)、(v1,v8) 、(v3,v7) 、(v1,v6)納入到最小生成樹中,如圖。此時parent陣列值為{1,5,8,7,7,8,0,0,6},怎麼去解讀這個陣列現在這些數字的意義呢?
最右圖i=6的粗線連線可以得到,我們其實是有兩個連通的邊集合A與B中納入到最小生成樹中的,當parent[0] = 1,表示v0和v1已經在生成樹的邊集合A中。parent[1]=5,表示v5和v8在邊集合A中,parent[5]=8表示v5和v8在邊集合A中,parent[6]=0表示集合A暫時到頭,此時邊集合A有v0、v1、v5、v8、v6。我們檢視parent中沒有檢視值,parent[2]=8表示v2和v8在一個集合中,因此v2也在邊集合A中。再parent[3]=7、parent[4]=7和parentp[7]=0可知v3,v4,v7在另一個邊集B中。
9. 當i=7時,第10行,呼叫Find函式,會傳入引數edges[7]。begin=5。此時第21行,parent[5]=8>0,所以f=8,再迴圈得parent[8]=6。因parent[6]=0所以Find返回第10行得到n=6。而此時第11行,傳入引數edges[7]。end=6得到m=6。此時n=m,不再列印,繼續下一迴圈。這就告訴我們,因為邊(v5,v6)使得邊集合A形成環路。因此不能將它納入到最小生成樹中
10. 當i=8時,與上面相同,由於邊(v1,v2)使得邊集合A形成了環路。因此不能將它納入到最小生成樹,
11. 當i=9時,邊(v6,v7),第10行得到n=6,第11行得到m=7,因此parent[6]=7,列印“(6 7)19”,此時parent陣列值為{1,5,8,7,7,8,7,0,6}
12. 此後的迴圈均造成環路,最終最小生成樹好
定義:假設N=(V,{E})是連通網,則令最小生成樹的初始狀態為只有n個頂點而無邊的非連通圖T={V,{}},圖中每個頂點自成一個連通分量。在E中選擇代價最小的邊,若該邊依附的點T中不同的連通分量上,則將此邊加入到T中,否則捨去此邊而選擇下一條代價最小的邊。依次類推,直至T中所有頂點都在同一連通分量上為止。
對比兩個演算法,克魯斯卡爾演算法主要是針對邊來展開,邊數少時效率會非常高,所以對於稀疏圖有很大的優勢;而普里姆演算法對於稠密圖,即邊數非常多的情況會更好一些。