1. 程式人生 > >資料結構與演算法19-圖的最小生成樹

資料結構與演算法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中所有頂點都在同一連通分量上為止。

對比兩個演算法,克魯斯卡爾演算法主要是針對邊來展開,邊數少時效率會非常高,所以對於稀疏圖有很大的優勢;而普里姆演算法對於稠密圖,即邊數非常多的情況會更好一些。