最小生成樹:Kruskal算法

最小生成樹:Kruskal算法

概覽

Kruskal算法(克魯斯卡爾算法)是一種用來尋找最小生成樹的算法,由Joseph Kruskal在1956年發表。Kruskal算法是基於貪心的思想得到的。Kruskal算法在圖中存在相同權值的邊時有效。

問題

給定一個無向圖,如果它任意兩個頂點都聯通並且是一棵樹,那麼我們就稱之爲生成樹(Spanning Tree)。如果是帶權值的無向圖,那麼權值之和最小的生成樹,我們就稱之爲最小生成樹(MST, Minimum Spanning Tree)。
由Kruskal算法,我們可以求得帶權無向圖的最小生成樹的權值之和。

算法描述

算法思想

Kruskal算法是基於貪心的思想得到的。首先把所有的邊按照權值先從小到大排列,接着按照順序選取每條邊,如果這條邊的兩個端點不屬於同一集合,那麼就將兩個集合合併,直到所有的點都屬於同一個集合爲止。合併到一個集合時可以使用並查集優化。換言之,Kruskal算法就是基於並查集的貪心算法。

算法過程

  1. 記圖中有V個頂點,E條邊。
    Kruskal算法圖1
  2. 將所有邊按權值從小到大排序。排序完成後,我們選擇邊AD,這樣我們的圖就變成了下圖。
    Kruskal算法圖2
  3. 在剩下的邊裏繼續尋找權值最小的,這裏找到了CE,它的權值也是5。
    Kruskal算法圖3
  4. 依此類推找到DF,AB,BE。
    Kruskal算法圖4
  5. 下面繼續選擇到BC或EF。儘管現在長度爲8的邊是最小的未選擇的邊,但現在它們已經連通了(對於BC可以通過CE,EB連接,對於EF可以通過EB,BA,AD,DF連接),所以不需要選擇BC或EF。類似地,BD也不需要選擇。最後剩下EG和FG,選擇權值更小的EG。
    Kruskal算法圖5

算法的簡單證明

對圖的頂點數n做歸納,證明Kruskal算法對任意n階圖適用:
當n=1時,顯然能夠找到最小生成樹。
假設Kruskal算法對n=k階圖適用,那麼,在k+1階圖G中,把權值最小的邊的兩個端點u和v做合併操作,即把u與v合爲一個點v',把原來接在u和v的邊都接到v'上去,這樣就能夠得到一個k階圖G'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'的生成樹(因爲G'就是由G中的u,v兩點合併產生的),所以W(T')≤W(T-{<u,v>}),即W(T'+{<u,v>})≤W(T)。與W(T)<W(T'+{<u,v>})矛盾。假設不成立。所以T'+{<u,v>}是G的最小生成樹,Kruskal算法對k+1階圖也適用。
由數學歸納法,Kruskal算法得證。

程序代碼

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

const int node_num = 100;    //最大頂點數

struct Edge
{
    int u, v, w;
} e[node_num * node_num];    //邊的兩端點和權值

bool cmp(const Edge &a, const Edge &b)
{
    return a.w < b.w;
}

int main()
{
    int v_num;    //頂點數
    int a_num;    //邊數
    int sum = 0;    //生成樹權值之和
    int vset[node_num];    //記錄各頂點所在的樹
    int k = 1;    //記錄生成樹中頂點數量

    cout << "v_num:";
    cin >> v_num;
    cout << "a_num:";
    cin >> a_num;

    for (int i = 0; i < v_num; ++i)
    {
        vset[i] = i;    //初始化頂點所在的樹
    }

    for (int i = 0; i < a_num; ++i)
    {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }

    sort(e, e + a_num, cmp);    //按邊權值從小到大排列

    for (int index = 0; index < a_num; ++index)
    {
        int sn1 = vset[e[index].u], sn2 = vset[e[index].v];    //讀取頂點所在的樹
        if (sn1 != sn2)    //如果不在同一個樹內
        {
            sum += e[index].w;    //加上這條邊的權值
            ++k;
            for (int i = 0; i < v_num; ++i)    //遍歷每個頂點,把屬於sn2這棵樹的頂點全部加入sn1這棵樹
            {
                if (vset[i] == sn2)
                {
                    vset[i] = sn1;
                }
            }
        }
    }
    if (k == v_num)
    {
        cout << sum << endl;
    }
    else
    {
        cout << "E" << endl;    //如果k不等於頂點數,說明不存在最小生成樹
    }
    return 0;
}

運行結果:
Kruskal算法運行結果

算法優化

並查集與路徑壓縮概述

並查集(Union Find Sets)是一種用於管理分組的數據結構。它具備兩個操作:查詢元素a和元素b是否爲同一組、將元素a和b合併爲同一組。
有一組指針指向每個節點的父節點,根節點的指針指向自己,通過循環就可以找到根節點。
路徑壓縮,就是在每次查找時,找到根節點,然後令查找路徑上的每個節點都直接指向根節點。
路徑壓縮圖
將a,b節點合併到同一棵樹,就是將a節點的根節點的父節點指針指向b節點的根節點。
節點合併到同一棵樹

實現

int find(int x) {//查找根節點

    int p = x, t;

    while (uset[p] != p){
        p = uset[p];
    }//返回根節點

    while (x != p) {
        t = uset[x];
        uset[x] = p;
        x = t;
    }//路徑壓縮

    return x;
}  

例題

USACO agrinet / POJ 1258

描述

農民約翰被選爲他們鎮的鎮長!他其中一個競選承諾就是在鎮上建立起互聯網,並連接到所有的農場。當然,他需要你的幫助。
約翰已經給他的農場安排了一條高速的網絡線路,他想把這條線路共享給其他農場。爲了使花費最少,他想鋪設最短的光纖去連接所有的農場。你將得到一份各農場之間連接費用的列表,你必須找出能連接所有農場並所用光纖最短的方案。每兩個農場間的距離不會超過100000。

輸入輸出格式

第一行: 農場的個數,N(3<=N<=100)。
第二行..結尾:接下來的行包含了一個N*N的矩陣,表示每個農場之間的距離。當然,對角線將會是0,因爲線路從第i個農場到它本身的距離在本題中沒有意義。
只有一個輸出,是連接到每個農場的光纖的最小長度和。

樣例

SAMPLE INPUT:

4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0

SAMPLE OUTPUT:

28

程序代碼

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

const int node_num = 100;

struct Edge
{
    int u, v, w;
} e[node_num * node_num];

bool cmp(const Edge &a, const Edge &b)
{
    return a.w < b.w;
}

int main()
{
    int v_num;
    while (cin >> v_num)
    {
        int vi = 0, sum = 0, vset[node_num], k = 1;

        for (int i = 0; i < v_num; ++i)
        {
            vset[i] = i;
            for (int j = 0; j < v_num; ++j)
            {
                e[vi].u = i;
                e[vi].v = j;
                cin >> e[vi].w;
                ++vi;
            }
        }
        sort(e, e + vi, cmp);
        for (int index = 0; index < vi; ++index)
        {
            int sn1 = vset[e[index].u], sn2 = vset[e[index].v];
            if (sn1 != sn2)
            {
                sum += e[index].w;
                ++k;
                for (int i = 0; i < v_num; ++i)
                {
                    if (vset[i] == sn2)
                    {
                        vset[i] = sn1;
                    }
                }
            }
        }
        cout << sum << endl;
    }
    return 0;
}

POJ 3723

描述

Windy有一個村莊,他想建立一支軍隊來保護他的村莊。他選了N個女孩和M個男孩,想讓他們做自己的士兵。在沒有任何關係的情況下,他僱傭一個士兵需要10000元。有一些女孩和男孩之間存在關係,Windy可以利用這些關係省一些花費。如果女孩x和男孩y間存在關係d且他們中的一個人已經被僱傭了,那麼Windy可以用10000-d元僱傭他們中的另一個。現在提供女孩和男孩之間的所有關係,你的任務是算出Windy最少要花多少錢。注意,僱傭一個士兵的時候最多隻能用一個關係。

輸入輸出格式

第1行:測試數據組數。
每組測試數據第1行:3個整數N,M,R。
下面R行,每行包含3個整數xi,yi,di。
在每組測試數據後有一個空行。

1≤N,M≤10000
0≤R≤50000

每組輸入數據對應一行輸出數據。

樣例

SAMPLE INPUT:

2

5 5 8
4 3 6831
1 3 4583
0 0 6592
0 1 3063
3 3 4975
1 3 2049
4 2 2104
2 2 781

5 5 10
2 4 9820
3 2 6236
3 1 8864
2 4 8326
2 0 5156
2 0 1463
4 1 2439
0 4 4373
3 4 8889
2 4 3133

SAMPLE OUTPUT:

71071
54223

程序代碼

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

const int node_num = 20000;

int u[50000], v[50000], w[50000], pre[node_num], p[50000];

bool cmp(const int &a, const int &b)
{
    return w[a] > w[b];
}

int findroot(const int);

int main()
{
    int n;
    scanf("%d", &n);
    for (int N = 0; N < n; ++N)
    {
        int ngirl, nboy, vi, sum = 0;
        scanf("%d%d%d", &ngirl, &nboy, &vi);
        for (int i = 0; i < ngirl + nboy; ++i)
        {
            pre[i] = i;
        }
        for (int i = 0; i < vi; ++i)
        {
            p[i] = i;
        }
        for (int i = 0; i < vi; ++i)
        {
            int t;
            scanf("%d%d%d", &u[i], &t, &w[i]);
            v[i] = t + ngirl;
        }
        sort(p, p + vi, cmp);
        for (int i = 0; i < vi; ++i)
        {
            int index = p[i];
            int rootx = findroot(u[index]), rooty = findroot(v[index]);
            if (rootx != rooty)
            {
                sum += w[index];
                pre[rooty] = rootx;
            }
        }
        sum = (ngirl + nboy) * 10000 - sum;
        printf("%d\n", sum);
    }
    return 0;
}

int findroot(const int x)
{
    if (pre[x] == x)
    {
        return x;
    }
    return pre[x] = findroot(pre[x]);
}

參考

  1. 最小生成樹之Kruskal算法
  2. 最小生成樹-Prim算法和Kruskal算法
  3. 【模版】並查集 及路徑壓縮
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章