Fork me on GitHub

图论中最小生成树之Kruskal算法

图论中最小生成树之Kruskal算法

1.背景

Kruskal算法是一种用来寻找最小生成树的算法,基于并查集和贪心算法,由Joseph Kruskal在1956年发表。

用来解决同样问题的还有Prim算法和Boruvka算法等(现在还有一种比较新的算法——solin算法,这个算法的时间复杂度为(nlogm),即在最差的情况下也比Prim算法(nlogn)快,因为(nlogm)几乎达不到)。

三种算法都是贪婪算法的应用,和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。

2.伪代码

int kruskal(int n,int m)
{
/**初始化父节点;
将图的边按权值排序;
每次取权值最小的边;
查:判断每条边的两端点是否属于同一集合,
即是否有相同的根节点;
并:若是同一集合,则舍去,反之合并到同一个根节点下;
将选中的边的权值累加,并保存于变量ans中;
*/
}

3.代码实现

#include<bits/stdc++.h>
#define maxn 1024
using namespace std;

typedef struct Edge
{
    int a;
    int b;
    int w;
}edge;

//定义全局变量,方便
int n,m;

edge a[maxn];
int fa[maxn];//每个点的父节点
int ra[maxn];//?每个点的阶数?

//自建判断函数
int cmp(const void *a,const void *b)
{
    return ((edge*)a)->w - ((edge*)b)->w;
}

//初始化树
void init(int n)
{
    for(int i = 0;i<n;i++)
    {
        ra[i] = 0;
        fa[i] = i;
    }
}

//查找点的父结点
int finde(int x)
{
/*
    int root = x;
    while(root != fa[root])
    {
        root = fa[root];
    }
    while(x != root)
    {
        int t = fa[x];
        fa[x] = root;
        x = t;
    }
    return root;
}
*/

//两种都可以
        if(fa[x] == x)
            return x;
        else
           finde(fa[x]);
    }

//连结两点
void unite(int x,int y)
{
    x = finde(x);
    y = finde(y);
    if(ra[x] >= ra[y])
    {
        fa[y] = x;
        if(ra[x] == ra[y])
            ra[x]++;
    }
    else
    {
        fa[x] = y;
    }
}

//查找最短路,返回最小值
//n为边,m为点
int kruskal(int n,int m)
{
    int nEdge = 0;
    int ans = 0;
    qsort(a,n,sizeof(a[0]),cmp);
    for(int i = 0;i<n && nEdge != m-1;i++)
    {
        //判断一条边的两个点是否为同一棵树
        if(finde(a[i].a) != finde(a[i].b))
        {
                unite(a[i].a,a[i].b);
                ans += a[i].w;
                nEdge++;
        }
    }
    //如果跳出循环,即加入的边的数量小于m-1,则表明改无向图不连通,即不存在最小生成数
    if(nEdge < m-1)
    {
        ans = -1;
    }
    return ans;
}
int main()
{
    int Ans;
    //n = 0 时退出
    while(scanf("%d%d",&n,&m),n)
    {
        init(m);
        for(int i = 0;i<n;i++)
        {
            scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].w);
        }

        Ans = kruskal(n,m);

        if(Ans == -1)
        {
            printf("?\n");
        }
        else
        {
            printf("%d\n",Ans);
        }
    }
    return 0;
}

运行结果:

kruskal的时间复杂度

  • Kruskal算法每次要从都要从剩余的边中选取一个最小的边。通常我们要先对边按权值从小到大排序,这一步的时间复杂度为为O(nlogn)。
  • Kruskal算法的实现通常使用并查集,来快速判断两个顶点是否属于同一个集合。最坏的情况可能要枚举完所有的边,此时要循环n次,所以这一步的时间复杂度为O(nα(V)),其中α为Ackermann函数,其增长非常慢,可以视为常数。
  • 所以Kruskal算法的时间复杂度为O(nlogn)

大礼包

你渴望力量吗?你渴望变强吗?你渴望原地爆炸吗?

有位老兄为我们准备了图论大礼包!!!

在此感谢大佬@Enstein_Jun

参考文章

学渣并不想跟你讲话,并向你扔了一道oj题 畅通工程

附上@Enstein_Jun的题解 传送门

Adhere to original technology sharing, your support will encourage me to continue to create!