1. 程式人生 > >prim最小生成樹演算法原理

prim最小生成樹演算法原理

prim 最小生成樹演算法原理 主要需要了解演算法的原理、演算法複雜度、優缺點 、刻畫和度量指標 評價等 可以查閱相關的文獻,這部分內容主要整合了兩篇部落格的內容

分別是:http://blog.csdn.net/tham_/article/details/46048907 這一篇重點在於演算法的複雜度

http://blog.csdn.net/hnust_xiehonghao/article/details/38013125 這一篇主要是幫助理解prim的演算法原理

這篇文章是對《演算法導論》上Prim演算法求無向連通圖最小生成樹的一個總結,其中有關於我的一點點小看法。

  最小生成樹的具體問題可以用下面的語言闡述:
    輸入

:一個無向帶權圖G=(V,E),對於每一條邊(u, v)屬於E,都有一個權值w。

    輸出:這個圖的最小生成樹,即一棵連線所有頂點的樹,且這棵樹中的邊的權值的和最小。

  舉例如下,求下圖的最小生成樹:

  這個問題是求解一個最優解的過程。那麼怎樣才算最優呢?

  首先我們考慮最優子結構:如果一個問題的最優解中包含了子問題的最優解,則該問題具有最優子結構。

  最小生成樹是滿足最優子結構的,下面會給出證明:

  最優子結構描述:假設我們已經得到了一個圖的最小生成樹(MST) T,(u, v)是這棵樹中的任意一條邊。如圖所示:

  現在我們把這條邊移除,就得到了兩科

子樹T1和T2,如圖:

  T1是圖G1=(V1, E1)的最小生成樹,G1是由T1的頂點匯出的圖G的子圖,E1={(x, y)∈E, x, y ∈V1}

  同理可得T2是圖G2=(V2, E2)的最小生成樹,G2是由T2的頂點匯出的圖G的子圖,E2={(x, y)∈E, x, y ∈V2}

  現在我們來證明上述結論:使用剪貼法。w(T)表示T樹的權值和。

    首先權值關係滿足:w(T) = w(u, v)+w(T1)+w(T2)

    假設存在一棵樹T1'比T1更適合圖G1,那麼就存在T'={(u,v)}UT1'UT2',那麼T'就會比T更適合圖G,這與T是最優解相矛盾。得證。

  因此最小生成樹具有最優子結構,那麼它是否還具有重疊子問題性質呢?我們可以發現,不管刪除那條邊,上述的最優子結構性質都滿足,都可以同樣求解,因此是滿足重疊子問題性質的。

  考慮到這,我們可能會想:那就說明最小生成樹可以用動態規劃來做咯?對,可以,但是它的代價是很高的。

  我們還能發現,它還有個更強大的性質:貪心選擇性質。因而可用貪心演算法完成。

  貪心演算法特點:一個區域性最優解也是全域性最優解。

  最小生成樹的貪心選擇性質:令T為圖G的最小生成樹,另A⊆V,假設邊(u, v)∈E是連線著A到A的補集(也就是V-A)的最小權值邊,那麼(u, v)屬於最小生成樹。

  證明:假設(u, v)∉T, 使用剪貼法。現在對下圖進行分析,圖中A的點用空心點表示,V-A的點用實心點表示:

  在T樹中,考慮從u到v的一條簡單路徑(注意現在(u, v)不在T中),根據樹的性質,它是唯一的。

    現在把(u, v)和這條路上中的第一條連線A和V-A的邊交換,即畫紅槓的那條邊,邊(u, v)是連線A和V-A的權值最小邊,那我們就得到了一棵更小的樹,這就與T是最小  生成樹矛盾。得證。

  現在呢,我們來看看Prim的思想:Prim演算法的特點是集合E中的邊總是形成單棵樹。樹從任意根頂點s開始,並逐漸形成,直至該樹覆蓋了V中所有頂點。每次新增到樹中的邊都是使樹的權值儘可能小的邊。因而上述策略是“貪心”的。

  演算法的輸入是無向連通圖G=(V, E)和待生成的最小生成樹的根r。在演算法的執行過程中,不在樹中的所有頂點都放在一個基於key域的最小優先順序佇列Q中。對每個頂點v來說,key[v]是所有將v與樹中某一頂點相連的邊中的最小權值;按規定如果不存在這樣的邊,則key[v]=∞。

  實現Prim演算法的虛擬碼如下所示:

  MST-PRIM(G, w, r)

    for each u∈V

      do key[u] ← ∞

         parent[u]← NIL

    key[r] ← 0

    Q ← V

    while Q ≠∅

      do u ← EXTRACT-MIN(Q)

        for each v∈Adj[u]

          do if v∈Q and w(u, v) < key[v]

            then parent[v] ← u

                key[v] ← w(u, v)

  其工作流程為:

    (1)首先進行初始化操作,將所有頂點入優先佇列,佇列的優先順序為權值越小優先順序越高

    (2)取佇列頂端的點u,找到所有與它相鄰且不在樹中的頂點v,如果w(u, v) < key[v],說明這條邊比之前的更優,加入到樹中,即更改父節點和key值。這中間還    隱含著更新Q的操作(降key值)

    (3)重複2操作,直至佇列空為止。

    (4)最後我們就得到了兩個陣列,key[v]表示樹中連線v頂點的最小權值邊的權值,parent[v]表示v的父結點。

  現在呢,我們發現一個問題,這裡要用到優先佇列來實現這個演算法,而且每次搜尋鄰接表都要進行佇列更新的操作。

  不管用什麼方法,總共用時為O(V*T(EXTRACTION)+E*T(DECREASE))

    (1)如果用陣列來實現,總時間複雜度為O(V2)

    (2)如果用二叉堆來實現,總時間複雜度為O(ElogV)

    (3)如果使用斐波那契堆,總時間複雜度為O(E+VlogV)

  上面的三種方法,越往下時間複雜度越好,但是實現難度越高,而且每次對最小優先佇列的更新是非常麻煩的,那麼,有沒有一種方法,可以不更新優先佇列也達到同樣的  效果呢?

  答案是:有。

  其實只需要簡單的操作就可以達到。首次只將根結點入佇列。第一次迴圈,取出佇列頂結點,將其退佇列,之後找到佇列頂的結點的所有相鄰頂點,若有更新,則更新它們的key值後,再將它們壓入佇列。重複操作直至佇列空為止。因為對樹的更新是區域性的,所以只需將相鄰頂點key值更新即可。push操作的複雜度為O(logV),而且省去了之前將所有頂點入佇列的時間,因而總複雜度為O(ElogV)。

  具體實現程式碼,鄰接矩陣優先佇列可以用STL實現:

  1. #include <iostream>
  2. #include <cstdio>
  3. #include <vector>
  4. #include <queue>
  5. usingnamespace std;  
  6. #define maxn 110  //最大頂點個數
  7. int n, m;       //頂點數,邊數
  8. struct arcnode  //邊結點
  9. {  
  10.     int vertex;     //與表頭結點相鄰的頂點編號
  11.     int weight;     //連線兩頂點的邊的權值
  12.     arcnode * next; //指向下一相鄰接點
  13.     arcnode() {}  
  14.     arcnode(int v,int w):vertex(v),weight(w),next(NULL) {}  
  15. };  
  16. struct vernode      //頂點結點,為每一條鄰接表的表頭結點
  17. {  
  18.     int vex;    //當前定點編號
  19.     arcnode * firarc;   //與該頂點相連的第一個頂點組成的邊
  20. }Ver[maxn];  
  21. void Init()  //建立圖的鄰接表需要先初始化,建立頂點結點
  22. {  
  23.     for(int i = 1; i <= n; i++)  
  24.     {  
  25.         Ver[i].vex = i;  
  26.         Ver[i].firarc = NULL;  
  27.     }  
  28. }  
  29. void Insert(int a, int b, int w)  //尾插法,插入以a為起點,b為終點,權為w的邊,效率不如頭插,但是可以去重邊
  30. {  
  31.     arcnode * q = new arcnode(b, w);  
  32.     if(Ver[a].firarc == NULL)  
  33.         Ver[a].firarc = q;  
  34.     else
  35.     {  
  36.         arcnode * p = Ver[a].firarc;  
  37.         if(p->vertex == b)  
  38.         {  
  39.             if(p->weight > w)  
  40.                 p->weight = w;  
  41.             return ;  
  42.         }  
  43.         while(p->next != NULL)  
  44.         {  
  45.             if(p->next->vertex == b)  
  46.             {  
  47.                 if(p->next->weight > w);  
  48.                     p->next->weight = w;  
  49.                 return ;  
  50.             }  
  51.             p = p->next;  
  52.         }  
  53.         p->next = q;  
  54.     }  
  55. }  
  56. void Insert2(int a, int b, int w)   //頭插法,效率更高,但不能去重邊
  57. {  
  58.     arcnode * q = new arcnode(b, w);  
  59.     if(Ver[a].firarc == NULL)  
  60.         Ver[a].firarc = q;  
  61.     else
  62.     {  
  63.         arcnode * p = Ver[a].firarc;  
  64.         q->next = p;  
  65.         Ver[a].firarc = q;  
  66.     }  
  67. }  
  68. struct node     //儲存key值的結點
  69. {  
  70.     int v;  
  71.     int key;  
  72.     friendbool operator<(node a, node b)   //自定義優先順序,key小的優先
  73.     {  
  74.         return a.key > b.key;  
  75.     }  
  76. };  
  77. #define INF 0xfffff    //權值上限
  78. int parent[maxn];   //每個結點的父節點
  79. bool visited[maxn]; //是否已經加入樹種
  80. node vx[maxn];      //儲存每個結點與其父節點連線邊的權值
  81. priority_queue<node> q; //優先佇列stl實現