圖的基本算法(最小生成樹(shù))

假設(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í)使用到了貪婪算法。

算法描述:

  1. 在一個(gè)加權(quán)連通圖中,頂點(diǎn)集合V,邊集合為E
  2. 任意選出一個(gè)點(diǎn)作為初始頂點(diǎn),標(biāo)記為visit,計(jì)算所有與之相連接的點(diǎn)的距離,選擇距離最短的,標(biāo)記visit.
  3. 重復(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ù)

1

2 選擇頂點(diǎn)a后,頂點(diǎn)啊置成visit(涂黑),計(jì)算周?chē)c它連接的點(diǎn)的距離:
2

3 與之相連的點(diǎn)距離分別為7,6,4,選擇C點(diǎn)距離最短,涂黑C,同時(shí)將這條邊高亮加入最小生成樹(shù):
3

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)的最近距離,很明顯,ba7bc的距離為6,更新b和已訪問(wèn)的點(diǎn)集距離為6,而f,ec的距離分別是8,9,所以還是涂黑b,高亮邊bc
4

5 接下來(lái)很明顯,d距離b最短,將d涂黑,bd高亮:
5

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

7 最后只有e了:
7

針對(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;

1

2 在算法中首先取出所有的邊,將邊按照長(zhǎng)短排序,然后首先取出最短的邊,將a,e放入同一個(gè)集合里,在實(shí)現(xiàn)中我們使用到了并查集的概念:
2

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

4 繼續(xù)找,找到第三短的邊ab,因?yàn)?code>a,e已經(jīng)在一個(gè)集合里,再將b加入:
4

5 繼續(xù)找,找到b,e,因?yàn)?code>b,e已經(jīng)同屬于一個(gè)集合,連起來(lái)的話就形成環(huán)了,所以邊be不加入最小生成樹(shù):
5

6 再找,找到bc,因?yàn)?code>c,d是一個(gè)集合的,a,b,e是一個(gè)集合,所以再合并這兩個(gè)集合:
6

這樣所有的點(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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容