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

最小生成樹(1)--Kruskal演算法

圖的最小生成樹

圖的最小生成樹,是指用最小的邊讓圖連通,讓任意兩點之間可以互相到達。圖如果有n個頂點,則應該有n-1條邊。此時連通無向圖沒有迴路,就是一顆樹,所以稱為最小生成樹。

最小生成樹是讓邊的總長度之和最短,其中一種方法是可以選擇最短的邊,然後依次選擇次短的邊,直到選擇了n-1條邊為止。所以可以先將所有的邊按權值從小到大進行排序(為了排序方便可以用結構體儲存圖的邊值資料),然後再選擇,選擇n-1條邊讓整個圖連通。當然有的權值小的邊是不能選擇的,那些會形成迴路的邊不能選,只能跳過這條邊。

在這個過程中,最難實現的是判斷兩個頂點有沒有連通。可以使用深度優先搜尋或者廣度優先搜尋。不過有更方便的方法,判斷頂點是否連通,可以用並查集查頂點是否有同一個祖先,如果有同一個祖先,就是連通的,如果不是同樣的祖先,就表示沒有連通,就可以選擇這兩個頂點的邊。

舉個栗子:
比如下面的圖,用kruskal演算法找最小生成樹的方法如下。
原圖:
原圖
選擇就先找權值最小的邊,很明顯是頂點1和2的權值只有1,而且兩個頂點現在沒有連通,則選擇這條邊將1和2連通。接下來的選擇頂點1和3的邊,權值次小為2,而且此時1和3沒有連通。所以可以選這條邊。如此選擇下去,直到選滿n-1條邊,也就是5條結束。

注意在最後一條邊,也就是下圖的標號5的邊,是1、2、3連通,4、5、6連通時,本來按照從小到大應該玄子頂點4和5的邊為7,但是此時頂點4和5已經通過頂點6連通了,就不需要再選擇這條邊了,也就是我們之前說的通過並查集看是否連通要跳過邊的情況。
查詢過程圖

找完所有邊之後,形成的最小生成樹:
最小生成樹

接下來小小的略微說一說並查集:

並查集

並查集就像是一片森林,不斷的合併成一棵大樹。要判斷兩個結點是否是同一棵樹,要注意必須求兩個結點的根源,也就是兩個結點的根結點是同一個則在同一個集合中。

所以我們用一個數組表示結點,用陣列的值記錄它的根結點可以不斷更新。首先,我們將結點都當成獨立的,也就是將陣列值初始化為它的下標,用之後按要求將結點合併。合併也就是建立並查集的過程。

怎麼實現呢?
兩個結點用遞迴先搜尋到它的根結點,比較兩個根結點是否相同。如果相同,就不用處理,若不相同,我們就規定靠左,也就是把陣列下標小的作為大的根,就是把陣列右邊的集合,作為左邊集合的子集合。用遞迴的好處還有,在每一次函式返回的時如果根結點有變,則順帶將之前同一棵樹下的結點都改成同一個根。

實現程式碼:

//遞迴尋求結點的祖先
int getf(int v)
{
    if(f[v]==v)
        return v;
    else
    {
        f[v]=getf(f[v]);
        return f[v];
    }
}

//合併並查集兩子集
int merge(int u, int v)
{
    int comp1,comp2;
    comp1=getf(u);
    comp2=getf(v);

    if(comp1==comp2)
        return 0;
    else
    {
        f[comp1]=comp2;
        return 1;
    }
    return 0;
}

講好了並查集,我們就可以用並查集來做最小生成樹了。在這裡我們統計一下最小生成樹的邊權值總和。下面貼出例項程式碼:

#include <stdio.h>
#include <stdlib.h>

int n,m,sum,count;
struct edge
{
    int u;
    int v;
    int w;
};
struct edge e[10];
int f[7]={0},book[10];

//快速排序
void quickSorted(int left, int right)
{
    int i,j;
    struct edge t;
    if(left>right)
        return;

    i = left;
    j = right;
    while(i!=j)
    {
        //一定要先從右往左找
        while(e[j].w>=e[left].w && i<j)
            j--;
        while(e[i].w<=e[left].w &&i<j)
            i++;
        //交換兩數的位置
        if(i<j)
        {
            t=e[i];
            e[i]=e[j];
            e[j]=t;
        }
    }
    //將基準數歸位
    t=e[left];
    e[left]=e[i];
    e[i]=t;
    quickSorted(left,i-1);//遞迴繼續處理左邊的序列
    quickSorted(i+1,right);//遞迴繼續處理右邊的序列
    return;
}

//遞迴尋求結點的祖先
int getf(int v)
{
    if(f[v]==v)
        return v;
    else
    {
        f[v]=getf(f[v]);
        return f[v];
    }
}

//合併並查集兩子集
int merge(int u, int v)
{
    int comp1,comp2;
    comp1=getf(u);
    comp2=getf(v);

    if(comp1==comp2)
        return 0;
    else
    {
        f[comp1]=comp2;
        return 1;
    }
    return 0;
}

//主程式
int main(void)
{
    int i;
    //讀入n,m表示頂點個數和邊的條數
    printf("Please input the numbers of Graph' vertex and edge divided by a space:\n");
    scanf("%d %d",&n,&m);

    //輸入邊,儲存邊的關係
    printf("Please input the two vertexes and their edge divided by space.\n");
    for(i=1;i<=m;i++)
        scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].w);

    quickSorted(1,m);//將邊進行排序

    //並查集初始化
    for(i=1;i<=n;i++)
        f[i]=i;

    //Kruskal演算法
    for(i=1;i<=m;i++)
    {
        //判斷邊的兩個頂點是否連通
        if(merge(e[i].u,e[i].v))
        {
            book[i]=1;//記錄哪些邊構成了最小生成樹
            count++;
            sum=sum+e[i].w;
        }
        if(count == n-1)
            break;
    }

    //輸出最小生成樹的邊
    printf("Print the smallest tree:\n");
    for(i=1;i<=m;i++)
    {
        if(book[i]==1)
            printf("%d %d %d\n",e[i].u,e[i].v,e[i].w);
        else
            continue;
     }

    printf("The wight of all: %d.\n",sum);
    return 0;
}

結果:
kruskal演算法結果圖

以上就是最小生成樹的kruskal演算法的內容啦,之後還有最小生成樹的另一種方法Prim演算法,這個和最短路徑的Dijkstra演算法很相似。待續…