1. 程式人生 > >最小生成樹-Prim演算法和Kruskal演算法

最小生成樹-Prim演算法和Kruskal演算法

假設以下情景,有一塊木板,板上釘上了一些釘子,這些釘子可以由一些細繩連線起來。假設每個釘子可以通過一根或者多根細繩連線起來,那麼一定存在這樣的情況,即用最少的細繩把所有釘子連線起來。
更為實際的情景是這樣的情況,在某地分佈著N個村莊,現在需要在N個村莊之間修路,每個村莊之前的距離不同,問怎麼修最短的路,將各個村莊連線起來。
以上這些問題都可以歸納為最小生成樹問題,用正式的表述方法描述為:給定一個無方向的帶權圖G=(V, E),最小生成樹為集合TT是以最小代價連線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高亮:
這裡寫圖片描述
f距離d7,距離b4,更新它的最短距離值是4,所以塗黑f,高亮bf
這裡寫圖片描述
7 最後只有e了:
這裡寫圖片描述

針對如上的圖,程式碼例項如下:

?
#include<iostream>
#define INF 10000
using namespace std;
constint N = 6;
bool visit[N];
intdist[N] = { 0, };
intgraph[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}
                  };
intprim(intcur)
{
    intindex = cur;
    intsum = 0;
    inti = 0;
    intj = 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++)
    {
        intminor = 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;
    returnsum;               //返回最小生成樹的總路徑值
}
intmain()
{
    cout << prim(0) << endl;//從頂點a開始
    return0;
}


Kruskal演算法

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

下面還是用一組圖示來表現演算法的過程:
1 初始情況,一個聯通圖,定義針對邊的資料結構,包括起點,終點,邊長度:

?
1 2 3 4 5 typedef struct _node{ intval;   //長度 intstart; //邊的起點 intend;   //邊的終點 }Node;


3 繼續找到第二短的邊,將cd再放入同一個集合裡:

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{
    intval;
    intstart;
    intend;
}Node;
Node V[N];
intcmp(constvoid *a, constvoid *b)
{
    return(*(Node *)a).val - (*(Node*)b).val;
}
intedge[N][3] = {  { 0,1,3},
                    {0,4,1}, 
                    {1,2,5}, 
                    {1,4,4},
                    {2,3,2}, 
                    {2,4,6}, 
                    {3,4,7}
                    };
 
intfather[N] = { 0, };
intcap[N] = {0,};
 
voidmake_set()              //初始化集合,讓所有的點都各成一個集合,每個集合都只包含自己
{
    for(inti = 0; i < N; i++)
    {
        father[i] = i;
        cap[i] = 1;
    }
}
 
intfind_set(intx)              //判斷一個點屬於哪個集合,點如果都有著共同的祖先結點,就可以說他們屬於一個集合
{
    if(x != father[x])
     {                             
        father[x] = find_set(father[x]);
    }    
    returnfather[x];
}                                 
 
voidUnion(intx, inty)         //將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);
    }
}
 
intKruskal(intn)
{
    intsum = 0;
    make_set();
    for(inti = 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;
        }
    }
    returnsum;
}
intmain()
{
    for(inti = 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;



*****************************************************************************

Prim演算法

1.概覽

普里姆演算法Prim演算法),圖論中的一種演算法,可在加權連通圖裡搜尋最小生成樹。意即由此演算法搜尋到的邊子集所構成的樹中,不但包括了連通圖裡的所有頂點英語Vertex (graph theory),且其所有邊的權值之和亦為最小。該演算法於1930年由捷克數學家沃伊捷赫·亞爾尼克英語Vojtěch Jarník發現;並在1957年由美國電腦科學家羅伯特·普里姆英語Robert C. Prim獨立發現;1959年,艾茲格·迪科斯徹再次發現了該演算法。因此,在某些場合,普里姆演算法又被稱為DJP演算法、亞爾尼克演算法或普里姆-亞爾尼克演算法。

2.演算法簡單描述

1).輸入:一個加權連通圖,其中頂點集合為V,邊集合為E;

2).初始化:Vnew = {x},其中x為集合V中的任一節點(起始點),Enew = {},為空;

3).重複下列操作,直到Vnew = V:

a.在集合E中選取權值最小的邊<u, v>,其中u為集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);

b.將v加入集合Vnew中,將<u, v>邊加入集合Enew中;

4).輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。

下面對演算法的圖例描述

圖例 說明 不可選 可選 已選(Vnew
 

此為原始的加權連通圖。每條邊一側的數字代表其權值。 - - -

頂點D被任意選為起始點。頂點ABEF通過單條邊與D相連。A是距離D最近的頂點,因此將A及對應邊AD以高亮表示。 C, G A, B, E, F D
 

下一個頂點為距離DA最近的頂點。BD為9,距A為7,E為15,F為6。因此,FDA最近,因此將頂點F與相應邊DF以高亮表示。 C, G B, E, F A, D
演算法繼續重複上面的步驟。距離A為7的頂點B被高亮表示。 C B, E, G A, D, F
 

在當前情況下,可以在CEG間進行選擇。CB為8,EB為7,GF為11。E最近,因此將頂點E與相應邊BE高亮表示。 C, E, G A, D, F, B
 

這裡,可供選擇的頂點只有CGCE為5,GE為9,故選取C,並與邊EC一同高亮表示。 C, G A, D, F, B, E

頂點G是唯一剩下的頂點,它距F為11,距E為9,E最近,故高亮表示G及相應邊EG G A, D, F, B, E, C

現在,所有頂點均已被選取,圖中綠色部分即為連通圖的最小生成樹。在此例中,最小生成樹的權值之和為39。 A, D, F, B, E, C, G

3.簡單證明prim演算法

反證法:假設prim生成的不是最小生成樹

1).設prim生成的樹為G0

2).假設存在Gmin使得cost(Gmin)<cost(G0)   則在Gmin中存在<u,v>不屬於G0

3).將<u,v>加入G0中可得一個環,且<u,v>不是該環的最長邊(這是因為<u,v>∈Gmin)

4).這與prim每次生成最短邊矛盾

5).故假設不成立,命題得證.

 4.演算法程式碼實現(未檢驗)

複製程式碼
#define MAX  100000
#define VNUM  10+1                                             //這裡沒有ID為0的點,so id號範圍1~10

int edge[VNUM][VNUM]={/*輸入的鄰接矩陣*/};
int lowcost[VNUM]={0};                                         //記錄Vnew中每個點到V中鄰接點的最短邊
int addvnew[VNUM];                                             //標記某點是否加入Vnew
int adjecent[VNUM]={0};                                        //記錄V中與Vnew最鄰近的點


void prim(int start)
{
     int sumweight=0;
     int i,j,k=0;

     for(i=1;i<VNUM;i++)                                      //頂點是從1開始
     {
        lowcost[i]=edge[start][i];
        addvnew[i]=-1;                                         //將所有點至於Vnew之外,V之內,這裡只要對應的為-1,就表示在Vnew之外
     }

     addvnew[start]=0;                                        //將起始點start加入Vnew
     adjecent[start]=start;
                                                 
     for(i=1;i<VNUM-1;i++)                                        
     {
        int min=MAX;
        int v=-1;
        for(j=1;j<VNUM;j++)                                      
        {
            if(addvnew[j]!=-1&&lowcost[j]<min)                 //在Vnew之外尋找最短路徑
            {
                min=lowcost[j];
                v=j;
            }
        }
        if(v!=-1)
        {
            printf("%d %d %d\n",adjecent[v],v,lowcost[v]);
            addvnew[v]=0;                                      //將v加Vnew

            sumweight+=lowcost[v];                             //計算路徑長度之和
            for(j=1;j<VNUM;j++)
            {
                if(addvnew[j]==-1&&edge[v][j]<lowcost[j])      
                {
                    lowcost[j]=edge[v][j];                     //此時v點加入Vnew 需要更新lowcost
                    adjecent[j]=v;                             
                }
            }
        }
    }
    printf("the minmum weight is %d",sumweight);
}
複製程式碼

5.時間複雜度

這裡記頂點數v,邊數e

鄰接矩陣:O(v2)                 鄰接表:O(elog2v)

Kruskal演算法

1.概覽

Kruskal演算法是一種用來尋找最小生成樹的演算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim演算法和Boruvka演算法等。三種演算法都是貪婪演算法的應用。和Boruvka演算法不同的地方是,Kruskal演算法在圖中存在相同權值的邊時也有效。

2.演算法簡單描述

1).記Graph中有v個頂點,e個邊

2).新建圖Graphnew,Graphnew中擁有原圖中相同的e個頂點,但沒有邊

3).將原圖Graph中所有e個邊按權值從小到大排序

4).迴圈:從權值最小的邊開始遍歷每條邊 直至圖Graph中所有的節點都在同一個連通分量中

                if 這條邊連線的兩個節點於圖Graphnew中不在同一個連通分量中

                                         新增這條邊到圖Graphnew

圖例描述:

首先第一步,我們有一張圖Graph,有若干點和邊 

將所有的邊的長度排序,用排序的結果作為我們選擇邊的依據。這裡再次體現了貪心演算法的思想。資源排序,對區域性最優的資源進行選擇,排序完成後,我們率先選擇了邊AD。這樣我們的圖就變成了右圖

在剩下的變中尋找。我們找到了CE。這裡邊的權重也是5

依次類推我們找到了6,7,7,即DF,AB,BE。

下面繼續選擇, BC或者EF儘管現在長度為8的邊是最小的未選擇的邊。但是現在他們已經連通了(對於BC可以通過CE,EB來連線,類似的EF可以通過EB,BA,AD,DF來接連)。所以不需要選擇他們。類似的BD也已經連通了(這裡上圖的連通線用紅色表示了)。

最後就剩下EG和FG了。當然我們選擇了EG。最後成功的圖就是右:

3.簡單證明Kruskal演算法

對圖的頂點數n做歸納,證明Kruskal演算法對任意n階圖適用。

歸納基礎:

n=1,顯然能夠找到最小生成樹。

歸納過程:

假設Kruskal演算法對n≤k階圖適用,那麼,在k+1階圖G中,我們把最短邊的兩個端點a和b做一個合併操作,即把u與v合為一個點v',把原來接在u和v的邊都接到v'上去,這樣就能夠得到一個k階圖G'(u,v的合併是k+1少一條邊),G'最小生成樹T'可以用Kruskal演算法得到。

我們證明T'+{<u,v>}是G的最小生成樹。

用反證法,如果T'+{<u,v>}不是最小生成樹,最小生成樹是T,即W(T)<W(T'+{<u,v>})。顯然T應該包含<u,v>,否則,可以用<u,v>加入到T中,形成一個環,刪除環上原有的任意一條邊,形成一棵更小權值的生成樹。而T-{<u,v>},是G'的生成樹。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),產生了矛盾。於是假設不成立,T'+{<u,v>}是G的最小生成樹,Kruskal演算法對k+1階圖也適用。

由數學歸納法,Kruskal演算法得證。

4.程式碼演算法實現

複製程式碼
typedef struct          
{        
    char vertex[VertexNum];                                //頂點表         
    int edges[VertexNum][VertexNum];                       //鄰接矩陣,可看做邊表         
    int n,e;                                               //圖中當前的頂點數和邊數         
}MGraph; 
 
typedef struct node  
{  
    int u;                                                 //邊的起始頂點   
    int v;                                                 //邊的終止頂點   
    int w;                                                 //邊的權值   
}Edge; 

void kruskal(MGraph G)  
{  
    int i,j,u1,v1,sn1,sn2,k;  
    int vset[VertexNum];                                    //輔助陣列,判定兩個頂點是否連通   
    int E[EdgeNum];                                         //存放所有的邊   
    k=0;                                                    //E陣列的下標從0開始   
    for (i=0;i<G.n;i++)  
    {  
        for (j=0;j<G.n;j++)  
        {  
            if (G.edges[i][j]!=0 && G.edges[i][j]!=INF)  
            {  
                E[k].u=i;  
                E[k].v=j;  
                E[k].w=G.edges[i][j];  
                k++;  
            }  
        }  
    }     
    heapsort(E,k,sizeof(E[0]));                            //堆排序,按權值從小到大排列       
    for (i=0;i<G.n;i++)                                    //初始化輔助陣列   
    {  
        vset[i]=i;  
    }  
    k=1;                                                   //生成的邊數,最後要剛好為總邊數   
    j=0;                                                   //E中的下標   
    while (k<G.n)  
    {   
        sn1=vset[E[j].u];  
        sn2=vset[E[j].v];                                  //得到兩頂點屬於的集合編號   
        if (sn1!=sn2)                                      //不在同一集合編號內的話,把邊加入最小生成樹   
        {
            printf("%d ---> %d, %d",E[j].u,E[j].v,E[j].w);       
            k++;  
            for (i=0;i<G.n;i++)  
            {  
                if (vset[i]==sn2)  
                {  
                    vset[i]=sn1;  
                }  
            }             
        }  
        j++;  
    }  
}  
複製程式碼

時間複雜度:elog2e  e為圖中的邊數

http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html