資料結構之Kruskal演算法(並查集的應用)

文中序發表於2017-07-14

Kruskal演算法基本思想

假設G=(V,E)是連通圖,將G中的邊按權值從小到大的順序排列
1、將n個頂點看成n個集合
2、按權值從大到小的順序選擇邊,所選邊應滿足兩個頂點不在同一個頂點集合內,即加入此邊後不會在生成樹中產生迴路,將該邊放到生成樹邊的集合中。同時將該邊的兩個頂點所在的頂點集合合併。
3、重複2,直到所有的頂點都在同一個頂點集合內。

舉個例子

這裡寫圖片描述

1、首先比較圖中的所有邊的權值,找到最小的權值的邊(1,6),加入生成樹的邊集中,TE={(1,6)}
2、比較圖中其餘邊的權值,找到最小的權值的邊(3,5),且加入此邊後不會使TE產生迴路,TE={(1,6),(3,5)}
3、比較圖中其餘邊的權值,找到最小的權值的邊(2,4),且加入此邊後不會使TE產生迴路,TE={(1,6),(3,5),(2,4)}
4、比較圖中其餘邊的權值,找到最小的權值的邊(5,6),且加入此邊後不會使TE產生迴路,TE={(1,6),(3,5),(2,4),(5,6)}
5、繼續上述演算法,找到邊(1,3),但加入後將會使得TE產生迴路,故舍棄。再找另外一條權值最小的邊,找到邊(3,6),但加入後也會使得TE產生迴路,故舍棄繼續尋找。找到最小權值邊(2,6)滿足條件,故將(2,6)加入TE中,此時TE={(1,6),(3,5),(2,4),(5,6),(2,6)}

現在所生成的最小生成樹中已經有了n-1條邊。此演算法完成。

並查集並查集—-百度百科 參考勇幸|Thinking的部落格

並查集:(union-find sets)
一種簡單的用途廣泛的集合. 並查集是若干個不相交集合,能夠實現較快的合併和判斷元素所在集合的操作,應用很多,如其求無向圖的連通分量個數等。最完美的應用當屬:實現Kruskar演算法求最小生成樹。

並查集的精髓(即它的三種操作,結合實現程式碼模板進行理解):
1、Make_Set(x) 把每一個元素初始化為一個集合
初始化後每一個元素的父親節點是它本身,每一個元素的祖先節點也是它本身(也可以根據情況而變)。

2、Find_Set(x) 查詢一個元素所在的集合
查詢一個元素所在的集合,其精髓是找到這個元素所在集合的祖先!這個才是並查集判斷和合並的最終依據。判斷兩個元素是否屬於同一集合,只要看他們所在集合的祖先是否相同即可。
合併兩個集合,也是使一個集合的祖先成為另一個集合的祖先,具體見示意圖

3、Union(x,y) 合併x,y所在的兩個集合
合併兩個不相交集合操作很簡單:
利用Find_Set找到其中兩個集合的祖先,將一個集合的祖先指向另一個集合的祖先。

這裡寫圖片描述

並查集的優化

1、Find_Set(x)時 路徑壓縮
尋找祖先時我們一般採用遞迴查詢,但是當元素很多亦或是整棵樹變為一條鏈時,每次Find_Set(x)都是O(n)的複雜度,有沒有辦法減小這個複雜度呢?
答案是肯定的,這就是路徑壓縮,即當我們經過”遞推”找到祖先節點後,”回溯”的時候順便將它的子孫節點都直接指向祖先,這樣以後再次Find_Set(x)時複雜度就變成O(1)了,如下圖所示;可見,路徑壓縮方便了以後的查詢。

2、Union(x,y)時 按秩合併
即合併的時候將元素少的集合合併到元素多的集合中,這樣合併之後樹的高度會相對較小。

這裡寫圖片描述

主要程式碼



//建立一個新的集合,每一個子節點就是一個數,本身就是他的根節點
void Make_Set(int x)
{
    father[x] = x;
    R[x] = 0;
}

//通過遞迴向上查詢根節點,回溯時改變當前節點的父節點,直接指向根節點。
int Find_Set(int x)
{
    if(x != father[x])
        father[x] = Find_set(father[x]);
    return father[x];

}

//將根節點設定為-1的非遞迴方法
int Find_Set2(int x)
{
    int y = x;
    while(y!= -1)
        y = father[y];
    return y;
}

//兩個集合的合併演算法
void Union(int x, int y)
{
    int GrandX = Find_set(x);
    int GrandY = Find_set(y);

    if(GrandX == GrandY)
        return;
    if(R[GrandX] < R[GrandY])
        father[GrandX] = GrandY;
    else
    {
        if(R[GrandX] == R[GrandY])
            R[GrandX]++;
        father[GrandY] = GrandX;    
    }
}

kruskal演算法完整版程式碼

#include<stdio.h>
#define max 50
typedef struct
{
    int bv,ev,w;
 } edges;
 edges edgeset[max];
 int createdgeset()
 {
    int arcnum,i;
    printf("\n input the undigraph: ");
    scanf("%d",&arcnum);
    for(i=1;i<=arcnum;i++)
    {
        printf("bv,ev,w=");
        scanf("%d,%d,%d",&edgeset[i].bv,&edgeset[i].ev,&edgeset[i].w);
     }
     return arcnum;
 }
 void sort(int n)
 {
    int i,j;
    edges t;
    for(i=1;i<=n-1;i++)
    for(j=i+1;j<=n;j++)
    if(edgeset[i].w>edgeset[j].w)
    {
        t=edgeset[j];
        edgeset[j]=edgeset[i];
        edgeset[i]=t;
     }
 }
 int seeks(int set[],int v)
 {
    int i=v;
    while(set[i]>0)
    i=set[i];
    return i;
 }
 void kruskal(int e)
 {
    int  set[max],v1,v2,i;
    printf("kruskal 's spanning tree:\n");
    for(i=1;i<=max;i++)
    set[i]=0;
    i=0;
    while(i<e)
    {
        v1=seeks(set,edgeset[i].bv);
        v2=seeks(set,edgeset[i].ev);
        if(v1!=v2)
        {
            printf("(%d,%d)%d\n",edgeset[i].bv,edgeset[i].ev,edgeset[i].w);
            set[v1]=v2;
         }
         i++;
     }
 }
 int main()
 {
    int i,arcnum;
    arcnum=createdgeset();
    sort(arcnum);
    printf("the arcnum from little to big:");
    printf("\nbv ev w\n");
    for(i=1;i<=arcnum;i++)
    printf("%d %d %d\n",edgeset[i].bv,edgeset[i].ev,edgeset[i].w);
    kruskal(arcnum);
    return 0;
 }

執行截圖

這裡寫圖片描述

總結

kruskal演算法相比於prim演算法而言,要簡單不少,prim演算法多適用於頂點多的稠密圖,而kruskal演算法多適用於邊數多的稀疏圖。kruskal演算法其精髓在於運用了並查集的概念,並查集是一個好東西,有時間要好好研究一下它的應用

相關文章