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