最小生成樹算法【圖解】:一文帶你理解什么是Prim算法和Kruskal算法


假設以下情景,有一塊木板,板上釘上了一些釘子,這些釘子可以由一些細繩連接起來。假設每個釘子可以通過一根或者多根細繩連接起來,那么一定存在這樣的情況,即用最少的細繩把所有釘子連接起來。
更為實際的情景是這樣的情況,在某地分布着N個村庄,現在需要在N個村庄之間修路,每個村庄之前的距離不同,問怎么修最短的路,將各個村庄連接起來。
以上這些問題都可以歸納為最小生成樹問題,用正式的表述方法描述為:給定一個無方向的帶權圖G=(V, E),最小生成樹為集合T, T是以最小代價連接V中所有頂點所用邊E的最小集合。 集合T中的邊能夠形成一顆樹,這是因為每個節點(除了根節點)都能向上找到它的一個父節點。

解決最小生成樹問題已經有前人開道,Prime算法和Kruskal算法,分別從點和邊下手解決了該問題。

Prim算法

Prim算法是一種產生最小生成樹的算法。該算法於1930年由捷克數學家沃伊捷赫·亞爾尼克(英語:Vojtěch Jarník)發現;並在1957年由美國計算機科學家羅伯特·普里姆(英語:Robert C. Prim)獨立發現;1959年,艾茲格·迪科斯徹再次發現了該算法。

Prim算法從任意一個頂點開始,每次選擇一個與當前頂點集最近的一個頂點,並將兩頂點之間的邊加入到樹中。Prim算法在找當前最近頂點時使用到了貪婪算法。

證明

從任意一個結點開始,將結點分成兩類:已加入的,未加入的。

每次從未加入的結點中,找一個與已加入的結點之間邊權最小值最小的結點。

然后將這個結點加入,並連上那條邊權最小的邊。

重復 \(n-1\) 次即可。

證明:還是說明在每一步,都存在一棵最小生成樹包含已選邊集。

基礎:只有一個結點的時候,顯然成立。

歸納:如果某一步成立,當前邊集為 \(F\) ,屬於 \(T\) 這棵 MST,接下來要加入邊 \(e\)

如果 \(e\) 屬於 \(T\) ,那么成立。

否則考慮 \(T+e\) 中環上另一條可以加入當前邊集的邊 \(f\)

首先, \(f\) 的權值一定不小於 \(e\) 的權值,否則就會選擇 \(f\) 而不是 \(e\) 了。

然后, \(f\) 的權值一定不大於 \(e\) 的權值,否則 \(T+e-f\) 就是一棵更小的生成樹了。

因此, \(e\)\(f\) 的權值相等, \(T+e-f\) 也是一棵最小生成樹,且包含了 \(F\)

算法描述:

  1. 在一個加權連通圖中,頂點集合V,邊集合為E
  2. 任意選出一個點作為初始頂點,標記為book,計算所有與之相連接的點的距離,選擇距離最短的,標記book.
  3. 重復以下操作,直到所有點都被標記為book
    在剩下的點鍾,計算與已標記book點距離最小的點,標記book,證明加入了最小生成樹。

下面我們來看一個最小生成樹生成的過程:
1 起初,從頂點a開始生成最小生成樹

2 選擇頂點a后,頂點a置成book(塗黑),計算周圍與它連接的點的距離:

3 與之相連的點距離分別為7,4,選擇C點距離最短,塗黑C,同時將這條邊高亮加入最小生成樹:

4 計算與a,c相連的點的距離(已經塗黑的點不計算),因為與a相連的已經計算過了,只需要計算與c相連的點,如果一個點與a,c都相連,那么它與a的距離之前已經計算過了,如果它與c的距離更近,則更新距離值,這里計算的是未塗黑的點距離塗黑的點的最近距離,很明顯,ba7bc的距離為6,更新b和已訪問的點集距離為6,而f,ec的距離分別是8,9,所以還是塗黑b,高亮邊bc

5 接下來很明顯,d距離b最短,將d塗黑,bd高亮:

6 f距離d7,距離b4,更新它的最短距離值是4,所以塗黑f,高亮bf

7 最后只有e了:

針對如上的圖,代碼實例如下(配合注釋理解):

#include<iostream>
#define INF 10000
using namespace std;
const int N = 6;
bool book[N];
int dist[N] = { 0 };
int graph[N][N] = { {INF,7,4,INF,INF,INF},   //INF代表兩點之間不可達
                    {7,INF,6,2,INF,4},
                    {4,6,INF,INF,9,8},
                    {INF,2,INF,INF,INF,7},
                    {INF,INF,9,INF,INF,1},
                    {INF,4,8,7,1,INF}
};
int Prim(int cur) {//選擇起始點
    int index = cur;
    int sum = 0;
    int i = 0;
    int j = 0;
    cout << index << " ";//輸出index可以輸出路徑
    memset(book, false, sizeof(book));//初始化
    book[cur] = true;//標記初始點
    for (; i < N; ++i)
        dist[i] = graph[cur][i];//初始化,並令每個與cur點鄰接點的距離存入dist
    for (i = 1; i < N; ++i) {
        int minor = INF;
        for (j = 0; j < N; ++j) {//找到與index相接的最短路徑
            if (!book[j] && dist[j] < minor) {
                minor = dist[j];
                index = j;
            }
        }
        book[index] = true;
        cout << index << " ";
        sum += minor;
        for (j = 0; j < N; ++j) {//重新初始化dist,找到與index鄰接的點
            if (!book[j] && dist[j] > graph[index][j])
                dist[j] = graph[index][j];
        }
    }
    cout << endl;
    return sum;//返回最小生成樹的總路徑值
}
int main() {
    //遍歷每個點為起始點
    for (int i = 0; i < N; ++i)
        cout << Prim(i) << endl;
    //cout<<Prim(0) << endl;//從頂點0開始
    return 0;
}

Kruskal算法

Kruskal是另一個計算最小生成樹的算法,其算法原理如下。首先,將每個頂點放入其自身的數據集合中。然后,按照權值的升序來選擇邊。當選擇每條邊時,判斷定義邊的頂點是否在不同的數據集中。如果是,將此邊插入最小生成樹的集合中,同時,將集合中包含每個頂點的聯合體取出,如果不是,就移動到下一條邊。重復這個過程直到所有的邊都探查過。

前置知識

並查集貪心圖的存儲

證明

思路很簡單,為了造出一棵最小生成樹,我們從最小邊權的邊開始,按邊權從小到大依次加入,如果某次加邊產生了環,就扔掉這條邊,直到加入了 \(n-1\) 條邊,即形成了一棵樹。

證明:使用歸納法,證明任何時候 K 算法選擇的邊集都被某棵 MST 所包含。

基礎:對於算法剛開始時,顯然成立(最小生成樹存在)。

歸納:假設某時刻成立,當前邊集為 \(F\) ,令 \(T\) 為這棵 MST,考慮下一條加入的邊 \(e\)

如果 \(e\) 屬於 \(T\) ,那么成立。

否則, \(T+e\) 一定存在一個環,考慮這個環上不屬於 \(F\) 的另一條邊 \(f\) (一定只有一條)。

首先, \(f\) 的權值一定不會比 \(e\) 小,不然 \(f\) 會在 \(e\) 之前被選取。

然后, \(f\) 的權值一定不會比 \(e\) 大,不然 \(T+e-f\) 就是一棵比 \(T\) 還優的生成樹了。

所以, \(T+e-f\) 包含了 \(F\) ,並且也是一棵最小生成樹,歸納成立。


下面還是用一組圖示來表現算法的過程:
1 初始情況,一個聯通圖,定義針對邊的數據結構,包括起點,終點,邊長度:

typedef struct _node{
    int val;   //長度
    int start; //邊的起點
    int end;   //邊的終點
}Node;

img

2 在算法中首先取出所有的邊,將邊按照長短排序,然后首先取出最短的邊,將a,e放入同一個集合里,在實現中我們使用到了並查集的概念:

如果有小伙伴不懂並查集的話,請點傳送門

img

3 繼續找到第二短的邊,將c, d再放入同一個集合里:

img

4 繼續找,找到第三短的邊ab,因為a,e已經在一個集合里,再將b加入:

img

5 繼續找,找到b,e,因為b,e已經同屬於一個集合,連起來的話就形成環了,所以邊be不加入最小生成樹:

img

6 再找,找到bc,因為c,d是一個集合的,a,b,e是一個集合,所以再合並這兩個集合:

img

這樣所有的點都歸到一個集合里,生成了最小生成樹。

根據上圖實現的代碼如下:

#include<iostream>
#define N 7
using namespace std;
struct Node {
    int val;   //長度
    int start; //邊的起點
    int end;   //邊的終點
};
Node V[N];
int cmp(const void *a, const void *b) {
    return (*(Node *)a).val - (*(Node*)b).val;
}
//edge保存結點屬性
int edge[N][3] = { { 0, 1, 3 },
                    { 0, 4, 1 },
                    { 1, 2, 5 },
                    { 1, 4, 4 },
                    { 2, 3, 2 },
                    { 2, 4, 6 },
                    { 3, 4, 7}
};
int father[N] = { 0 };
int cap[N] = { 0 };

//初始化集合,讓所有的點都各成一個集合,每個集合都只包含自己
//並查集初始化,先令每個結點的父節點為自己
void make_set() {
    for (int i = 0; i < N; ++i) {
        father[i] = i;
        cap[i] = 1;//集合大小(勢力大小)
    }
}

//遞歸尋找所屬集合的父節點
//並且在尋找父節點的同時重置所屬集合
int find_set(int x) {
    if (x != father[x])
        father[x] = find_set(father[x]);
    return father[x];
}

//將x,y合並到同一個集合
void Union(int x, int y) {
    x = find_set(x);
    y = find_set(y);
    if (x == y)
        return;
    if (cap[x] < cap[y])
        father[x] = find_set(y);
    else {//歸左思想
        if (cap[x] == cap[y])
            cap[x]++;
        father[y] = find_set(x);
    }
}

int Kruskal(int n) {
    int sum = 0;
    make_set();
    for (int i = 0; i < N; ++i) {
        if (find_set(V[i].start) != find_set(V[i].end)) {
            Union(V[i].start, V[i].end);
            sum += V[i].val;
        }
    }
    return sum;
}

int main() {
    for (int i = 0; i < N; ++i) {
        V[i].start = edge[i][0];
        V[i].end = edge[i][1];
        V[i].val = edge[i][2];
    }
    qsort(V, N, sizeof(V[0]), cmp);
    cout << Kruskal(0) << endl;
}

除去這兩種算法外還有——Boruvka 算法。

該算法的思想是前兩種算法的結合。它可以用於求解 邊權互不相同 的無向圖的最小生成森林。(無向連通圖就是最小生成樹。)

介紹博文:Here


免責聲明!

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



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