最小生成树:Kruskal算法

来源:互联网 发布:许晴 任盈盈 知乎 编辑:程序博客网 时间:2024/06/08 04:58

最小生成树: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. 【模版】并查集 及路径压缩
原创粉丝点击