最小生成樹算法


正文
      所謂最小生成樹,就是在一個具有N個頂點的帶權連通圖G中,如果存在某個子圖G',其包含了圖G中的所有頂點和一部分邊,且不形成回路,並且子圖G'的各邊權值之和最小,則稱G'為圖G的最小生成樹。
      由定義我們可得知最小生成樹的三個性質:
      • 最小生成樹不能有回路。
      • 最小生成樹可能是一個,也可能是多個。
      • 最小生成樹邊的個數等於頂點的個數減一。

本文將介紹兩種最小生成樹的算法,分別為克魯斯卡爾算法(Kruskal Algorithm)和普利姆算法(Prim Algorithm)。

一、克魯斯卡爾算法(Kruskal Algorithm)

克魯斯卡爾算法的核心思想是:在帶權連通圖中,不斷地在邊集合中找到最小的邊,如果該邊滿足得到最小生成樹的條件,就將其構造,直到最后得到一顆最小生成樹。

克魯斯卡爾算法的執行步驟:
第一步:在帶權連通圖中,將邊的權值排序;
第二步:判斷是否需要選擇這條邊(此時圖中的邊已按權值從小到大排好序)。判斷的依據是邊的兩個頂點是否已連通,如果連通則繼續下一條;如果不連通,那么就選擇使其連通。
第三步:循環第二步,直到圖中所有的頂點都在同一個連通分量中,即得到最小生成樹。

下面我用圖示法來演示克魯斯卡爾算法的工作流程,如下圖:

 

首先,將圖中所有的邊排序(從小到大),我們將以此結果來選擇。排序后各邊按權值從小到大依次是:

HG < (CI=GF) < (AB=CF) < GI < (CD=HI) < (AH=BC) < DE < BH < DF

接下來,我們先選擇HG邊,將這兩個點加入到已找到點的集合。這樣圖就變成了,如圖

 

繼續,這次選擇邊CI(當有兩條邊權值相等時,可隨意選一條),此時需做判斷。

判斷法則:當將邊CI加入到已找到邊的集合中時,是否會形成回路?
     1.如果沒有形成回路,那么直接將其連通。此時,對於邊的集合又要做一次判斷:這兩個點是否在已找到點的集合中出現過?
          ①.如果兩個點都沒有出現過,那么將這兩個點都加入已找到點的集合中;
          ②.如果其中一個點在集合中出現過,那么將另一個沒有出現過的點加入到集合中;
          ③.如果這兩個點都出現過,則不用加入到集合中。
     2.如果形成回路,不符合要求,直接進行下一次操作。

根據判斷法則,不會形成回路,將點C和點I連通,並將點C和點I加入到集合中。如圖:

繼續,這次選擇邊GF,根據判斷法則,不會形成回路,將點G和點F連通,並將點F加入到集合中。如圖:

繼續,這次選擇邊AB,根據判斷法則,不會形成回路,將其連通,並將點A和點B加入到集合中。如圖:

繼續,這次選擇邊CF,根據判斷法則,不會形成回路,將其連通,此時這兩個點已經在集合中了,所以不用加入。如圖:

繼續,這次選擇邊GI,根據判斷法則,會形成回路,如下圖,直接進行下一次操作:

 繼續,這次選擇邊CD,根據判斷法則,不會形成回路,將其連通,並將點D加入到集合中。如圖:

繼續,這次選擇邊HI,根據判斷法則,會形成回路,直接進行下一次操作。
繼續,這次選擇邊AH,根據判斷法則,不會形成回路,將其連通,此時這兩個點已經在集合中了,所以不用加入。
繼續,這次選擇邊BC,根據判斷法則,會形成回路,直接進行下一次操作。
繼續,這次選擇邊DE,根據判斷法則,不會形成回路,將其連通,並將點E加入到集合中。如圖:

繼續,這次選擇邊BH,根據法則,會形成回路,進行下一次操作。
最后選擇邊DF,根據法則,會形成回路,不將其連通,也不用加入到集合中。

好了,所有的邊都遍歷完成了,所有的頂點都在同一個連通分量中,我們得到了這顆最小生成樹。

通過生成的過程可以看出,能否得到最小生成樹的核心問題就是上面所描述的判斷法則。
那么,我們如何用算法來描述判斷法則呢?我認為只需要三個步驟即可:
    ⒈將某次操作選擇的邊XY的兩個頂點X和Y和已找到點的集合作比較,如果
         ①這兩個點都在已找到點的集合中,那么return 2;
         ②這兩個點有一個在已找到點的集合中,那么return 1;
         ③這兩個點都不在一找到點的集合中,那么return 0;
    ⒉當返回值為0或1時,可判定不會形成回路;
    ⒊當返回值為2時,判定能形成回路的依據是:假如能形成回路,設能形成回路的點的集合中有A,B,C,D四個點,那么以點A為起始點,繞環路一周后必能回到點A。如果能回到,則形成回路;如果不能,則不能形成回路。

#include<iostream>
#include<algorithm>
using namespace std;

const int size = 128; 
int n;
int father[size];
int rank[size];

//把每條邊成為一個結構體,包括起點、終點和權值 
typedef struct node
{
    int val;
    int start;
    int end;    
}edge[SIZE * SIZE / 2];

//把每個元素初始化為一個集合 
void make_set()
{
    for(int i = 0; i < n; i ++){
        father[i] = i;
        rank[i] = 1;    
    }    
    return ;
}

//查找一個元素所在的集合,即找到祖先 
int find_set(int x)
{
    if(x != father[x]){
        father[x] = find_set(father[x]);    
    }    
    return father[x];
}

//合並x,y所在的兩個集合:利用Find_Set找到其中兩個
//集合的祖先,將一個集合的祖先指向另一個集合的祖先。
void Union(int x, int y)
{
    x = find_set(x);    
    y = find_set(y);
    if(x == y){
        return ;    
    }
    if(rank[x] < rank[y]){
        father[x] = find_set(y);    
    }
    else{
        if(rank[x] == rank[y]){
            rank[x] ++;    
        }    
        father[y] = find_set(x);
    }
    return ;
}

bool cmp(pnode a, pnode b)
{
    return a.val < b.val;    
}

int kruskal(int n) //n為邊的數量 
{
    int sum = 0;
    make_set();
    for(int i = 0; i < n; i ++){   //從權最小的邊開始加進圖中 
        if(find_set(edge[i].start) != find_set(edge[i].end)){
            Union(edge[i].start, edge[i].end);
            sum += edge[i].val;    
        }    
    }
    return sum;    
}

int main()
{
    while(1){
        scanf("%d", &n);    
        if(n == 0){
            break;    
        }
        char x, y;
        int m, weight;
        int cnt = 0;
        for(int i = 0; i < n - 1; i ++){
            cin >> x >> m; 
            //scanf("%c %d", &x, &m);    
            //printf("%c %d ", x, m);
            for(int j = 0; j < m; j ++){
                cin >> y >> weight; 
                //scanf("%c %d", &y, &weight);
                //printf("%c %d ", y, weight);    
                edge[cnt].start = x - 'A';
                edge[cnt].end = y - 'A';
                edge[cnt].val = weight;
                cnt ++;
            }
        }
        
        sort(edge, edge + cnt, cmp); //對邊按權從小到大排序 
        cout << kruskal(cnt) << endl; 
    }    
}
View Code

 

 二、普利姆算法(Prim Algorithm)

普利姆算法的核心步驟是:在帶權連通圖中,從圖中某一頂點v開始,此時集合U={v},重復執行下述操作:在所有u∈U,w∈V-U的邊(u,w)∈E中找到一條權值最小的邊,將(u,w)這條邊加入到已找到邊的集合,並且將點w加入到集合U中,當U=V時,就找到了這顆最小生成樹。

其實,由步驟我們就可以定義查找法則:在所有u∈U,w∈V-U的邊(u,w)∈E中找到一條權值最小的邊。

ok,知道了普利姆算法的核心步驟,下面我就用圖示法來演示一下工作流程,如圖:

首先,確定起始頂點。我以頂點A作為起始點。根據查找法則,與點A相鄰的點有點B和點H,比較AB與AH,我們選擇點B,如下圖。並將點B加入到U中。

繼續下一步,此時集合U中有{A,B}兩個點,再分別以這兩點為起始點,根據查找法則,找到邊BC(當有多條邊權值相等時,可選任意一條),如下圖。並將點C加入到U中。

繼續,此時集合U中有{A,B,C}三個點,根據查找法則,我們找到了符合要求的邊CI,如下圖。並將點I加入到U中。

繼續,此時集合U中有{A,B,C,I}四個點,根絕查找法則,找到符合要求的邊CF,如下圖。並將點F加入到集合U中。

繼續,依照查找法則我們找到邊FG,如下圖。並將點G加入到U中。

繼續,依照查找法則我們找到邊GH,如下圖。並將點H加入到U中。

 

 繼續,依照查找法則我們找到邊CD,如下圖。並將點D加入到U中。

 

 繼續,依照查找法則我們找到邊DE,如下圖。並將點E加入到U中。

 

此時,滿足U = V,即找到了這顆最小生成樹。
附注:prim算法最小生成樹的生成過程中,也有可能構成回路,需做判斷。判斷的方法和克魯斯卡爾算法一樣。

int prime(int cur)
{
    int index;
    int sum = 0;
    memset(visit, false, sizeof(visit));
    visit[cur] = true;
    for(int i = 0; i < m; i ++){
        dist[i] = graph[cur][i];    
    }
    
    for(int i = 1; i < m; i ++){
        
        int mincost = INF;
        for(int j = 0; j < m; j ++){
            if(!visit[j] && dist[j] < mincost){
                mincost = dist[j];
                index = j;    
            }    
        }
        
        visit[index] = true;
        sum += mincost;
        
        for(int j = 0; j < m; j ++){
            if(!visit[j] && dist[j] > graph[index][j]){
                dist[j] = graph[index][j];
            }    
        }    
    } 
    return sum;    
}
View Code

 

 

轉載:http://blog.csdn.net/fengchaokobe/article/details/7521780
        http://www.cnblogs.com/aiyelinglong/archive/2012/03/26/2418707.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM