假設(shè)以下情景,有一塊木板,板上釘上了一些釘子,這些釘子可以由一些細(xì)繩連接起來(lái)。假設(shè)每個(gè)釘子可以通過(guò)一根或者多根細(xì)繩連接起來(lái),那么一定存在這樣的情況,即用最少的細(xì)繩把所有釘子連接起來(lái)。
更為實(shí)際的情景是這樣的情況,在某地分布著N
個(gè)村莊,現(xiàn)在需要在N
個(gè)村莊之間修路,每個(gè)村莊之前的距離不同,問(wèn)怎么修最短的路,將各個(gè)村莊連接起來(lái)。
以上這些問(wèn)題都可以歸納為最小生成樹(shù)問(wèn)題,用正式的表述方法描述為:給定一個(gè)無(wú)方向的帶權(quán)圖G=(V, E)
,最小生成樹(shù)為集合T
, T
是以最小代價(jià)連接V
中所有頂點(diǎn)所用邊E
的最小集合。 集合T
中的邊能夠形成一顆樹(shù),這是因?yàn)槊總€(gè)節(jié)點(diǎn)(除了根節(jié)點(diǎn))都能向上找到它的一個(gè)父節(jié)點(diǎn)。
解決最小生成樹(shù)問(wèn)題已經(jīng)有前人開(kāi)道,Prime
算法和Kruskal
算法,分別從點(diǎn)和邊下手解決了該問(wèn)題。
Prim算法##
Prim
算法是一種產(chǎn)生最小生成樹(shù)的算法。該算法于1930
年由捷克數(shù)學(xué)家沃伊捷赫·亞爾尼克(英語(yǔ):Vojtěch Jarník
)發(fā)現(xiàn);并在1957
年由美國(guó)計(jì)算機(jī)科學(xué)家羅伯特·普里姆(英語(yǔ):Robert C. Prim
)獨(dú)立發(fā)現(xiàn);1959
年,艾茲格·迪科斯徹再次發(fā)現(xiàn)了該算法。
Prim
算法從任意一個(gè)頂點(diǎn)開(kāi)始,每次選擇一個(gè)與當(dāng)前頂點(diǎn)集最近的一個(gè)頂點(diǎn),并將兩頂點(diǎn)之間的邊加入到樹(shù)中。Prim
算法在找當(dāng)前最近頂點(diǎn)時(shí)使用到了貪婪算法。
算法描述:
- 在一個(gè)加權(quán)連通圖中,頂點(diǎn)集合
V
,邊集合為E
- 任意選出一個(gè)點(diǎn)作為初始頂點(diǎn),標(biāo)記為
visit
,計(jì)算所有與之相連接的點(diǎn)的距離,選擇距離最短的,標(biāo)記visit
. - 重復(fù)以下操作,直到所有點(diǎn)都被標(biāo)記為
visit
:
在剩下的點(diǎn)鐘,計(jì)算與已標(biāo)記visit
點(diǎn)距離最小的點(diǎn),標(biāo)記visit
,證明加入了最小生成樹(shù)。
下面我們來(lái)看一個(gè)最小生成樹(shù)生成的過(guò)程:
1 起初,從頂點(diǎn)a
開(kāi)始生成最小生成樹(shù)
2 選擇頂點(diǎn)
a
后,頂點(diǎn)啊置成visit
(涂黑),計(jì)算周?chē)c它連接的點(diǎn)的距離:3 與之相連的點(diǎn)距離分別為
7
,6
,4
,選擇C
點(diǎn)距離最短,涂黑C
,同時(shí)將這條邊高亮加入最小生成樹(shù):4 計(jì)算與
a,c
相連的點(diǎn)的距離(已經(jīng)涂黑的點(diǎn)不計(jì)算),因?yàn)榕ca
相連的已經(jīng)計(jì)算過(guò)了,只需要計(jì)算與c
相連的點(diǎn),如果一個(gè)點(diǎn)與a,c
都相連,那么它與a
的距離之前已經(jīng)計(jì)算過(guò)了,如果它與c的距離更近,則更新距離值,這里計(jì)算的是未涂黑的點(diǎn)距離涂黑的點(diǎn)的最近距離,很明顯,b
和a
為7
,b
和c
的距離為6
,更新b
和已訪問(wèn)的點(diǎn)集距離為6
,而f
,e
和c
的距離分別是8
,9
,所以還是涂黑b
,高亮邊bc
:5 接下來(lái)很明顯,
d
距離b
最短,將d
涂黑,bd
高亮:6
f
距離d
為7
,距離b
為4
,更新它的最短距離值是4
,所以涂黑f
,高亮bf
:7 最后只有
e
了:針對(duì)如上的圖,代碼實(shí)例如下:
#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代表兩點(diǎn)之間不可達(dá)
{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];//初始化,每個(gè)與a鄰接的點(diǎn)的距離存入dist
for (i = 1; i < N; i++)
{
int minor = INF;
for (j = 0; j < N; j++)
{
if (!visit[j] && dist[j] < minor) //找到未訪問(wèn)的點(diǎn)中,距離當(dāng)前最小生成樹(shù)距離最小的點(diǎn)
{
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]) //執(zhí)行更新,如果點(diǎn)距離當(dāng)前點(diǎn)的距離更近,就更新dist
{
dist[j] = graph[index][j];
}
}
}
cout << endl;
return sum; //返回最小生成樹(shù)的總路徑值
}
int main()
{
cout << prim(0) << endl;//從頂點(diǎn)a開(kāi)始
return 0;
}
Kruskal算法##
Kruskal是另一個(gè)計(jì)算最小生成樹(shù)的算法,其算法原理如下。首先,將每個(gè)頂點(diǎn)放入其自身的數(shù)據(jù)集合中。然后,按照權(quán)值的升序來(lái)選擇邊。當(dāng)選擇每條邊時(shí),判斷定義邊的頂點(diǎn)是否在不同的數(shù)據(jù)集中。如果是,將此邊插入最小生成樹(shù)的集合中,同時(shí),將集合中包含每個(gè)頂點(diǎn)的聯(lián)合體取出,如果不是,就移動(dòng)到下一條邊。重復(fù)這個(gè)過(guò)程直到所有的邊都探查過(guò)。
下面還是用一組圖示來(lái)表現(xiàn)算法的過(guò)程:
1 初始情況,一個(gè)聯(lián)通圖,定義針對(duì)邊的數(shù)據(jù)結(jié)構(gòu),包括起點(diǎn),終點(diǎn),邊長(zhǎng)度:
typedef struct _node{
int val; //長(zhǎng)度
int start; //邊的起點(diǎn)
int end; //邊的終點(diǎn)
}Node;
2 在算法中首先取出所有的邊,將邊按照長(zhǎng)短排序,然后首先取出最短的邊,將
a
,e
放入同一個(gè)集合里,在實(shí)現(xiàn)中我們使用到了并查集的概念:3 繼續(xù)找到第二短的邊,將
c
, d
再放入同一個(gè)集合里:4 繼續(xù)找,找到第三短的邊
ab
,因?yàn)?code>a,e
已經(jīng)在一個(gè)集合里,再將b
加入:5 繼續(xù)找,找到
b
,e
,因?yàn)?code>b,e
已經(jīng)同屬于一個(gè)集合,連起來(lái)的話就形成環(huán)了,所以邊be
不加入最小生成樹(shù):6 再找,找到
bc
,因?yàn)?code>c,d
是一個(gè)集合的,a
,b
,e
是一個(gè)集合,所以再合并這兩個(gè)集合:這樣所有的點(diǎn)都?xì)w到一個(gè)集合里,生成了最小生成樹(shù)。
根據(jù)上圖實(shí)現(xiàn)的代碼如下:
#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() //初始化集合,讓所有的點(diǎn)都各成一個(gè)集合,每個(gè)集合都只包含自己
{
for (int i = 0; i < N; i++)
{
father[i] = i;
cap[i] = 1;
}
}
int find_set(int x) //判斷一個(gè)點(diǎn)屬于哪個(gè)集合,點(diǎn)如果都有著共同的祖先結(jié)點(diǎn),就可以說(shuō)他們屬于一個(gè)集合
{
if (x != father[x])
{
father[x] = find_set(father[x]);
}
return father[x];
}
void Union(int x, int y) //將x,y合并到同一個(gè)集合
{
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++)//將邊的順序按從小到大取出來(lái)
{
if (find_set(V[i].start) != find_set(V[i].end)) //如果改變的兩個(gè)頂點(diǎn)還不在一個(gè)集合中,就并到一個(gè)集合里,生成樹(shù)的長(zhǎng)度加上這條邊的長(zhǎng)度
{
Union(V[i].start, V[i].end); //合并兩個(gè)頂點(diǎn)到一個(gè)集合
sum += V[i].val;
}
}
return sum;
}
int main()
{
for (int i = 0; i < N; i++) //初始化邊的數(shù)據(jù),在實(shí)際應(yīng)用中可根據(jù)具體情況轉(zhuǎn)換并且讀取數(shù)據(jù),這邊只是測(cè)試用例
{
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;
}