最小生成樹演算法【圖解】--一文帶你理解什麼是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
演算法在找當前最近頂點時使用到了貪婪演算法。
演算法描述:
- 在一個加權連通圖中,頂點集合
V
,邊集合為E
- 任意選出一個點作為初始頂點,標記為
book
book
. - 重複以下操作,直到所有點都被標記為
book
:
在剩下的點鐘,計算與已標記book
點距離最小的點,標記book
,證明加入了最小生成樹。
下面我們來看一個最小生成樹生成的過程:
1 起初,從頂點a
開始生成最小生成樹
2 選擇頂點a
後,頂點a
置成book
(塗黑),計算周圍與它連線的點的距離:
3 與之相連的點距離分別為7
,4
,選擇C
點距離最短,塗黑C
,同時將這條邊高亮加入最小生成樹:
4 計算與a,c
相連的點的距離(已經塗黑的點不計算),因為與a
相連的已經計算過了,只需要計算與c
相連的點,如果一個點與a,c
都相連,那麼它與a
b
和a
為7
,b
和c
的距離為6
,更新b
和已訪問的點集距離為6
,而f
,e
和c
的距離分別是8
,9
,所以還是塗黑b
,高亮邊bc
:
5 接下來很明顯,d
距離b
最短,將d
塗黑,bd
高亮:
6 f
距離d
為7
,距離b
為4
,更新它的最短距離值是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是另一個計算最小生成樹的演算法,其演算法原理如下。首先,將每個頂點放入其自身的資料集合中。然後,按照權值的升序來選擇邊。當選擇每條邊時,判斷定義邊的頂點是否在不同的資料集中。如果是,將此邊插入最小生成樹的集合中,同時,將集合中包含每個頂點的聯合體取出,如果不是,就移動到下一條邊。重複這個過程直到所有的邊都探查過。
下面還是用一組圖示來表現演算法的過程:
1 初始情況,一個聯通圖,定義針對邊的資料結構,包括起點,終點,邊長度:
typedef struct _node{
int val; //長度
int start; //邊的起點
int end; //邊的終點
}Node;
2 在演算法中首先取出所有的邊,將邊按照長短排序,然後首先取出最短的邊,將a
,e
放入同一個集合裡,在實現中我們使用到了並查集的概念:
如果有小夥伴不懂並查集的話,請點傳送門
3 繼續找到第二短的邊,將c
, d
再放入同一個集合裡:
4 繼續找,找到第三短的邊ab
,因為a
,e
已經在一個集合裡,再將b
加入:
5 繼續找,找到b
,e
,因為b
,e
已經同屬於一個集合,連起來的話就形成環了,所以邊be
不加入最小生成樹:
6 再找,找到bc
,因為c
,d
是一個集合的,a
,b
,e
是一個集合,所以再合併這兩個集合:
這樣所有的點都歸到一個集合裡,生成了最小生成樹。
根據上圖實現的程式碼如下:
#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;
}