圖基本算法 最小生成樹 Prim算法(鄰接表/鄰接矩陣+優先隊列STL)


  這篇文章是對《算法導論》上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 using namespace std;
  6 
  7 #define maxn 110  //最大頂點個數
  8 int n, m;       //頂點數,邊數
  9 
 10 struct arcnode  //邊結點
 11 {
 12     int vertex;     //與表頭結點相鄰的頂點編號
 13     int weight;     //連接兩頂點的邊的權值
 14     arcnode * next; //指向下一相鄰接點
 15     arcnode() {}
 16     arcnode(int v,int w):vertex(v),weight(w),next(NULL) {}
 17 };
 18 
 19 struct vernode      //頂點結點,為每一條鄰接表的表頭結點
 20 {
 21     int vex;    //當前定點編號
 22     arcnode * firarc;   //與該頂點相連的第一個頂點組成的邊
 23 }Ver[maxn];
 24 
 25 void Init()  //建立圖的鄰接表需要先初始化,建立頂點結點
 26 {
 27     for(int i = 1; i <= n; i++)
 28     {
 29         Ver[i].vex = i;
 30         Ver[i].firarc = NULL;
 31     }
 32 }
 33 
 34 void Insert(int a, int b, int w)  //尾插法,插入以a為起點,b為終點,權為w的邊,效率不如頭插,但是可以去重邊
 35 {
 36     arcnode * q = new arcnode(b, w);
 37     if(Ver[a].firarc == NULL)
 38         Ver[a].firarc = q;
 39     else
 40     {
 41         arcnode * p = Ver[a].firarc;
 42         if(p->vertex == b)
 43         {
 44             if(p->weight > w)
 45                 p->weight = w;
 46             return ;
 47         }
 48         while(p->next != NULL)
 49         {
 50             if(p->next->vertex == b)
 51             {
 52                 if(p->next->weight > w);
 53                     p->next->weight = w;
 54                 return ;
 55             }
 56             p = p->next;
 57         }
 58         p->next = q;
 59     }
 60 }
 61 void Insert2(int a, int b, int w)   //頭插法,效率更高,但不能去重邊
 62 {
 63     arcnode * q = new arcnode(b, w);
 64     if(Ver[a].firarc == NULL)
 65         Ver[a].firarc = q;
 66     else
 67     {
 68         arcnode * p = Ver[a].firarc;
 69         q->next = p;
 70         Ver[a].firarc = q;
 71     }
 72 }
 73 struct node     //保存key值的結點
 74 {
 75     int v;
 76     int key;
 77     friend bool operator<(node a, node b)   //自定義優先級,key小的優先
 78     {
 79         return a.key > b.key;
 80     }
 81 };
 82 
 83 #define INF 0xfffff    //權值上限
 84 int parent[maxn];   //每個結點的父節點
 85 bool visited[maxn]; //是否已經加入樹種
 86 node vx[maxn];      //保存每個結點與其父節點連接邊的權值
 87 priority_queue<node> q; //優先隊列stl實現
 88 void Prim()    //s表示根結點
 89 {
 90     for(int i = 1; i <= n; i++) //初始化
 91     {
 92         vx[i].v = i;
 93         vx[i].key = INF;
 94         parent[i] = -1;
 95         visited[i] = false;
 96     }
 97     vx[1].key = 0;
 98     q.push(vx[1]);
 99     while(!q.empty())
100     {
101         node nd = q.top();  //取隊首,記得趕緊pop掉
102         q.pop();
103         if(visited[nd.v])   //注意這一句的深意,避免很多不必要的操作
104             continue;
105         visited[nd.v] = true;
106         arcnode * p = Ver[nd.v].firarc;
107         while(p != NULL)    //找到所有相鄰結點,若未訪問,則入隊列
108         {
109             if(!visited[p->vertex] && p->weight < vx[p->vertex].key)
110             {
111                 parent[p->vertex] = nd.v;
112                 vx[p->vertex].key = p->weight;
113                 vx[p->vertex].v = p->vertex;
114                 q.push(vx[p->vertex]);
115             }
116             p = p->next;
117         }
118     }
119 }
120 
121 int main()
122 {
123     int a, b ,w;
124     cout << "輸入n和m: ";
125     cin >> n >> m;
126     Init();
127     cout << "輸入所有的邊:" << endl;
128     while(m--)
129     {
130         cin >> a >> b >> w;
131         Insert2(a, b, w);
132         Insert2(b, a, w);
133     }
134     Prim();
135     cout << "輸出所有結點的父結點:" << endl;
136     for(int i = 1; i <= n; i++)
137         cout << parent[i] << " ";
138     cout << endl;
139     cout << "最小生成樹權值為:";
140     int cnt = 0;
141     for(int i = 1; i <= n; i++)
142         cnt += vx[i].key;
143     cout << cnt << endl;
144     return 0;
145 }

 

 

(當明確知道沒有重邊時,用Insert2()進行插入能提高效率)

運行結果如下(基於第一個例子):

 

可用下列題進行測試:HDU搜索“暢通工程” POJ 1251

接下來是鄰接矩陣實現,非常簡單,但是有幾點還是需要注意的:

 

 1 #include <iostream>
 2 #include <cstdio>
 3 #include <queue>
 4 using namespace std;
 5 
 6 #define maxn 110
 7 #define INF 100020    //預定於的最大值
 8 int n;   //頂點數、邊數
 9 int g[maxn][maxn];      //鄰接矩陣表示
10 
11 struct node     //保存key值的結點
12 {
13     int v;
14     int key;
15     friend bool operator<(node a, node b)   //自定義優先級,key小的優先
16     {
17         return a.key > b.key;
18     }
19 };
20 int parent[maxn];   //每個結點的父節點
21 bool visited[maxn]; //是否已經加入樹種
22 node vx[maxn];      //保存每個結點與其父節點連接邊的權值
23 priority_queue<node> q; //優先隊列stl實現
24 void Prim()    //s表示根結點
25 {
26     for(int i = 1; i <= n; i++) //初始化
27     {
28         vx[i].v = i;
29         vx[i].key = INF;
30         parent[i] = -1;
31         visited[i] = false;
32     }
33     vx[1].key = 0;
34     q.push(vx[1]);
35     while(!q.empty())
36     {
37         node nd = q.top();  //取隊首,記得趕緊pop掉
38         q.pop();
39         if(visited[nd.v] == true)   //深意,因為push機器的可能是重復但是權值不同的點,我們只取最小的
40             continue;
41         int st = nd.v;
42         //cout << nd.v << " " << nd.key << endl;
43         visited[nd.v] = true;
44         for(int j = 1;  j <= n; j++)
45         {
46             if(j!=st && !visited[j] && g[st][j] < vx[j].key)    //判斷
47             {
48                 parent[j] = st;
49                 vx[j].key = g[st][j];
50                 q.push(vx[j]);
51 
52             }
53         }
54     }
55 }
56 int main()
57 {
58     while(~scanf("%d", &n))  //點的個數
59     {
60         for(int i = 1; i <= n; i++)  //輸入鄰接矩陣
61             for(int j = 1; j <= n; j++)
62             {
63                 scanf("%d", &g[i][j]);
64                 if(g[i][j] == 0)
65                     g[i][j] = INF;  //注意0的地方置為INF
66             }
67         Prim();  //調用
68         int ans = 0;  //權值和
69         for(int i = 1; i <= n; i++)
70             ans += vx[i].key;
71         printf("%d\n", ans);
72 
73     }
74     return 0;
75 }

 

題目:POJ 1258

望支持,謝謝。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM