本節綱要
- 什么是圖(network)
- 什么是最小生成樹 (minimum spanning tree)
- 最小生成樹的算法
什么是圖(network)?
這里的圖當然不是我們日常說的圖片或者地圖。通常情況下,我們把圖看成是一種由“頂點”和“邊”組成的抽象網絡。在各個“頂點“間可以由”邊“連接起來,使兩個頂點間相互關聯起來。圖的結構可以描述多種復雜的數據對象,應用較為廣泛,看下圖:

為了更好地說明問題,下面我們看一個比較老套的通信問題:
在各大城市中建設通信網絡,如下圖所示,每個圓圈代表一座城市,而邊上的數字代表了建立通信連接的價格。那么,請問怎樣才能以最小的價格使各大城市能直接或者間接地連接起來呢?

我們需要注意兩點:
- 最小的價格
- 各大城市可以是直接或者間接相連的
稍稍留心可以發現,題目的要求是,城市只需要直接或者間接相連,因此,為了節省成本,我們稍稍優化一下上述方案如下:

可以看到,我們砍掉了原先在AD,BE之間的兩條道路,建設價格自然就降下來了。當然這個方案也是符合我們題目的要求的。按照國際慣例,這里要說蛋是了。上面的實例由於數據很簡單,優化的方案很easy就看出來了。但在實際中,數據量往往是非常龐大的。所以,我們更傾向於設計一種方法,然后利用計算機強大的運算能力幫我們處理這些數據得出最優的方案。
那么,針對上述問題,我們一起來看看如何應用圖的相關知識來實現吧。
什么是最小生成樹(minimum spanning tree)
為了直觀,還是用圖片給大家解釋一下:

-
對於一個圖而言,它可以生成很多樹,如右側圖2,圖3就是由圖1生成的。
-
從上面可以看出生成樹是將原圖的全部頂點以最少的邊連通的子圖,對於有n個頂點的連通圖,生成樹有n-1條邊,若邊數小於此數就不可能將各頂點連通,如果邊的數量多於n-1條邊,必定會產生回路。
-
對於一個帶權連通圖,生成樹不同,樹中各邊上權值總和也不同,權值總和最小的生成樹則稱為圖的最小生成樹。
關於最小生成樹的算法(Prim算法和Kruskal算法)
Prim算法
基本思想:
假設有一個無向帶權圖G=(V,E),它的最小生成樹為MinTree=(V,T),其中V為頂點集合,T為邊的集合。求邊的集合T的步驟如下:
①令 U={u0},T={}。其中U為最小生成樹的頂點集合,開始時U中只含有頂點u0(u0可以為集合V中任意一項),在開始構造最小生成樹時我們從u0出發。
②對所有u∈U,v∈(V – U)(其中u,v表示頂點)的邊(u,v)中,找一條權值最小的邊(u’,v’),將這條邊加入到集合T中,將頂點v’加入集合U中。
③直到將V中所有頂點加入U中,則算法結束,否則一直重復以上兩步。
④符號說明:我們用大寫字母表示集合,用小寫字母表示頂點元素,用<>表示兩點之間的邊。
為了更好的說明問題,我們下面一步一步來為大家展示這個過程。
- 初始狀態:U={a} V={b,c,d,e } T={}
- 集合U和V相關聯的權值最小的邊是<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,於是我們將b加入U。U={a,b},V={d,c,e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
- 此時集合U和V相關聯的權值最小的邊是<b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,於是我們將c加入U。U={a,b,c} ,V={d,e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
- 顯然此時集合U和V中相關聯的權值最小的邊是<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,於是我們將d加入U。U={a,b,c,d} ,V={e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
- 最后集合U和V中相關聯的權值最小的邊是<d,e style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,於是將e加入U。U={a,b,c,d,e} ,V={},T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<d,e style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}。
到此所有點訪問完畢。
代碼實現
1//prime算法
2//將城市X標記為visit=true時,就表示該城市加入到集合U,用sum累加記錄邊的總費用
3
4#include<iostream>
5#define NO 99999999 //99999999代表兩點之間不可達
6#define N 5
7using namespace std;
8
9bool visit[N];
10long long money[N] = { 0 };
11long long graph[N][N] = {0};
12
13void initgraph()
14{
15 for (int i = 0; i < N; i++)
16 {
17 for (int j = 0; j < N; j++)
18 {
19 scanf(" %lld", &graph[i][j]);
20 }
21 }
22
23}
24
25void printgraph()
26{
27 for (int i = 0; i < N; i++)
28 {
29 for (int j = 0; j < N; j++)
30 {
31 printf(" %lld", graph[i][j]);
32 }
33 }
34
35}
36
37int prim(int city)
38{
39 initgraph();
40 printgraph();
41 int index = city;
42 int sum = 0;
43 int i = 0;
44 int j = 0;
45 cout <<"訪問節點:" <<index << "\n";
46 memset(visit, false, sizeof(visit));
47 visit[city] = true;
48 for (i = 0; i < N; i++)
49 {
50 money[i] = graph[city][i];//初始化,每個與城市city間相連的費用存入money,以便后續比較
51 }
52
53 for (i = 1; i < N; i++)
54 {
55 int minor = NO;
56 for (j = 0; j < N; j++)
57 {
58 if ((visit[j] == false) && money[j] < minor) //找到未訪問的城市中,與當前最小生成樹中的城市間費用最小的城市
59 {
60 minor = money[j];
61 index = j;
62 }
63 }
64 visit[index] = true;
65 cout << "訪問節點:" << index << "\n";
66 sum += minor; //求總的最低費用
67 /*這里是一個更新,如果未訪問城市與當前城市間的費用更低,就更新money,保存更低的費用*/
68 for (j = 0; j < N; j++)
69 {
70 if ((visit[j] == false) && money[j]>graph[index][j])
71 {
72 money[j] = graph[index][j];
73 }
74 }
75 }
76 cout << endl;
77 return sum; //返回總費用最小值
78}
79int main()
80{
81 cout << "修路最低總費用為:"<< prim(0) << endl;//從城市0開始
82 return 0;
83}
Kruskal算法
解最小生成樹的另一種常見的算法是Kruskal算法,它比Prim算法更直觀。
Kruskal算法的做法是:每次都從剩余邊中選取權值最小的,當然,這條邊不能使已有的邊產生回路。手動求解會發現Kruskal算法異常簡單,下面是一個例子

先對邊的權值排個序:
1(V0,V4)、2(V2,V6)、4(V1,V3)、6(V1,V2)、8(V3,V6)、10(V5,V6)、12(V3,V5)、15(V4,V5)、20(V0,V1)
首選邊1(V0,V4)、2(V2,V6)、4(V1,V3)、6(V1,V2),此時的圖是這樣

顯然,若選取邊8(V3,V6)則會出現環,則必須拋棄8(V3,V6),選擇下一條10(V5,V6)沒有問題,此時圖變成這樣

顯然,12(V3,V5)同樣不可取,選取15(V4,V5),邊數已達到要求,算法結束。最終的圖是這樣的

算法邏輯很容易理解,但用代碼判斷當前邊是否會引起環的出現則很棘手。這里簡單提一提連通分量
- 在無向圖中,如果從頂點vi到頂點vj有路徑,則稱vi和vj連通。如果圖中任意兩個頂點之間都連通,則稱該圖為連通圖,否則,將其中較大的連通子圖稱為連通分量。
- 在有向圖中,如果對於每一對頂點vi和vj,從vi到vj和從vj到vi都有路徑,則稱該圖為強連通圖;否則,將其中的極大連通子圖稱為強連通分量。
算法說明
為了判斷環的出現,我們換個角度來理解Kruskal算法的做法:初始時,把圖中的n個頂點看成是獨立的n個連通分量,從樹的角度看,也是n個根節點。我們選邊的標准是這樣的:若邊上的兩個頂點從屬於兩個不同的連通分量,則此邊可取,否則考察下一條權值最小的邊。
於是問題又來了,如何判斷兩個頂點是否屬於同一個連通分量呢?這個可以參照並查集的做法解決。它的思路是:如果兩個頂點的根節點是一樣的,則顯然是屬於同一個連通分量。這也同樣暗示着:在加入新邊時,要更新父節點。
1//kruskal算法
2
3#include<cstdio>
4#include<iostream>
5#include<cstring>
6#include<cstdlib>
7#include<algorithm>
8#include<cmath>
9#include<map>
10#include<set>
11#include<list>
12#include<vector>
13using namespace std;
14#define N 10005
15#define M 50005
16#define qm 100005
17#define INF 2147483647
18struct arr{
19 int ff, tt, ww;
20}c[M << 1];// 存儲邊的集合,ff,tt,ww為一條從ff連接到tt的權值為ww的邊
21int tot = 0;//邊的總數
22int ans = 0;//最小生成樹的權值和
23int f[N];//並查集
24bool comp(const arr & a, const arr & b){
25 return a.ww < b.ww;
26}
27int m, n;//邊數量,點數量
28int getfa(int x){
29 return f[x] == x ? x : f[x] = getfa(f[x]);
30}//並查集,帶路徑壓縮
31
32inline void add(int x, int y, int z){
33 c[++tot].ff = x;
34 c[tot].tt = y;
35 c[tot].ww = z;
36 return;
37}//新增一條邊
38
39void kruscal(){
40 for (int i = 1; i <= n; i ++) f[i] = i;
41 for (int i = 1; i <= m; i ++){
42 int fx = getfa(c[i].ff);//尋找祖先
43 int fy = getfa(c[i].tt);
44 if (fx != fy){//不在一個集合,合並加入一條邊
45 f[fx] = fy;
46 ans += c[i].ww;
47 }
48 }
49
50 return;
51}
52int main(){
53 freopen("input10.txt", "r", stdin);
54 freopen("output10.txt", "w", stdout);
55 scanf("%d%d",&n, &m);
56 int x, y, z;
57 for (int i = 1; i <= m; i ++){
58 scanf("%d %d %d", &x, &y, &z);
59 add(x, y, z);
60 }
61 sort(c + 1, c + 1 + m, comp);//快速排序
62 kruscal();
63 printf("%d\n", ans);
64 return 0;
65}