圖解Prim&Kruskal演算法

卡巴拉的樹發表於2017-12-18

假設以下情景,有一塊木板,板上釘上了一些釘子,這些釘子可以由一些細繩連線起來。假設每個釘子可以通過一根或者多根細繩連線起來,那麼一定存在這樣的情況,即用最少的細繩把所有釘子連線起來。 更為實際的情景是這樣的情況,在某地分佈著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演算法在找當前最近頂點時使用到了貪婪演算法。

圖解演算法流程:

1 . 在一個加權連通圖中,頂點集合V,邊集合為E

2 . 任意選出一個點作為初始頂點,標記為visit,計算所有與之相連線的點的距離,選擇距離最短的,標記visit.

3 . 重複以下操作,直到所有點都被標記為visit: 在剩下的點鐘,計算與已標記visit點距離最小的點,標記visit,證明加入了最小生成樹。

下面我們來看一個最小生成樹生成的過程:

1 起初,從頂點a開始生成最小生成樹

1
2 選擇頂點a後,頂點啊置成visit(塗黑),計算周圍與它連線的點的距離:
2
3 與之相連的點距離分別為7,6,4,選擇C點距離最短,塗黑C,同時將這條邊高亮加入最小生成樹:
3
4 計算與a,c相連的點的距離(已經塗黑的點不計算),因為與a相連的已經計算過了,只需要計算與c相連的點,如果一個點與a,c都相連,那麼它與a的距離之前已經計算過了,如果它與c的距離更近,則更新距離值,這裡計算的是未塗黑的點距離塗黑的點的最近距離,很明顯,ba7bc的距離為6,更新b和已訪問的點集距離為6,而f,ec的距離分別是8,9,所以還是塗黑b,高亮邊bc
4
5 接下來很明顯,d距離b最短,將d塗黑,bd高亮:
5
6 f距離d7,距離b4,更新它的最短距離值是4,所以塗黑f,高亮bf
6
7 最後只有e了:
7

程式碼實現:

#include<iostream>
#define INF 10000
using namespace std;
const int N = 6;
bool visit[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 << " ";
	memset(visit, false, sizeof(visit));
	visit[cur] = true;
	for (i = 0; i < N; i++)
		dist[i] = graph[cur][i];//初始化,每個與a鄰接的點的距離存入dist
	for (i = 1; i < N; i++)
	{
		int minor = INF;
		for (j = 0; j < N; j++)
		{
			if (!visit[j] && dist[j] < minor)          //找到未訪問的點中,距離當前最小生成樹距離最小的點
			{
				minor = dist[j];
				index = j;
			}
		}
		visit[index] = true;
		cout << index << " ";
		sum += minor;
		for (j = 0; j < N; j++)
		{
			if (!visit[j] && dist[j]>graph[index][j])      //執行更新,如果點距離當前點的距離更近,就更新dist
			{
				dist[j] = graph[index][j];
			}
		}
	}
	cout << endl;
	return sum;               //返回最小生成樹的總路徑值
}
int main()
{
	cout << prim(0) << endl;//從頂點a開始
	return 0;
}
複製程式碼

Kruskal演算法

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

圖解演算法流程:

1 初始情況,一個聯通圖,定義針對邊的資料結構,包括起點,終點,邊長度:

typedef struct _node{
	int val;   //長度
	int start; //邊的起點
	int end;   //邊的終點
}Node;
複製程式碼

1
2 在演算法中首先取出所有的邊,將邊按照長短排序,然後首先取出最短的邊,將a,e放入同一個集合裡,在實現中我們使用到了並查集的概念:
2
3 繼續找到第二短的邊,將c, d再放入同一個集合裡:
3
4 繼續找,找到第三短的邊ab,因為a,e已經在一個集合裡,再將b加入:
4
5 繼續找,找到b,e,因為b,e已經同屬於一個集合,連起來的話就形成環了,所以邊be不加入最小生成樹:
5
6 再找,找到bc,因為c,d是一個集合的,a,b,e是一個集合,所以再合併這兩個集合:
6
這樣所有的點都歸到一個集合裡,生成了最小生成樹。

程式碼實現:

#include<iostream>
#define N 7
using namespace std;
typedef struct _node{
	int val;
	int start;
	int end;
}Node;
Node V[N];
int cmp(const void *a, const void *b)
{
	return (*(Node *)a).val - (*(Node*)b).val;
}
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];
}                                  

void Union(int x, int y)         //將x,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;
	return 0;
}
複製程式碼

相關文章