1. 程式人生 > >圖的基本算法(最小生成樹)

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

原理 kruskal view cmp visit 針對 ems 科學家 -m

假設以下情景,有一塊木板。板上釘上了一些釘子。這些釘子能夠由一些細繩連接起來。假設每一個釘子能夠通過一根或者多根細繩連接起來。那麽一定存在這種情況,即用最少的細繩把全部釘子連接起來。
更為實際的情景是這種情況。在某地分布著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開始生成最小生成樹
技術分享圖片
2 選擇頂點a後,頂點啊置成visit(塗黑),計算周圍與它連接的點的距離:
技術分享圖片
3 與之相連的點距離分別為7

,6,4,選擇C點距離最短,塗黑C,同一時候將這條邊高亮增加最小生成樹:
技術分享圖片
4 計算與a,c相連的點的距離(已經塗黑的點不計算),由於與a相連的已經計算過了,僅僅須要計算與c相連的點,假設一個點與a,c都相連。那麽它與a的距離之前已經計算過了,假設它與c的距離更近。則更新距離值。這裏計算的是未塗黑的點距離塗黑的點的近期距離,非常明顯,ba7bc的距離為6。更新b和已訪問的點集距離為6,而f,ec的距離各自是8,9,所以還是塗黑b,高亮邊bc
技術分享圖片
5 接下來非常明顯。d距離b最短。將d塗黑。bd高亮:
技術分享圖片
6 f距離d7,距離b4。更新它的最短距離值是4。所以塗黑f,高亮bf
技術分享圖片
7 最後僅僅有e了:
技術分享圖片

針對如上的圖,代碼實比例如以下:

#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;

技術分享圖片
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;
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;
}

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