kruskal算法透彻理解(含并查集及最小生成树的解释)

//如果有疑问的话欢迎留言
克鲁斯卡尔(kruskal)算法通常用于求出一个连通图中的最小生成树,本文会对这种算法以及该算法的基础(最小生成树、并查集)进行详细的介绍。

最小生成树

首先明确一下概念,什么是最小生成树呢?
现在我有一张由n个节点构成的连通图(如下)
kruskal算法透彻理解(含并查集及最小生成树的解释)_第1张图片
可以看到,每个节点都有许多条边与其它节点相连,我们要让这张图变成最小生成树,只需要让每一个节点至少有一条边与其它点相连就可以了,而每个点的边权值是一定的,我们要在保证图的联通的情况下删去一些边,使得留下的边的边权和尽可能小,最后留下的这张图就会是原图的最小生成树(如下图)。
kruskal算法透彻理解(含并查集及最小生成树的解释)_第2张图片
看到了吗?现在的图删去了无用的边,在保证图的每个点直接或间接联通的情况下使得边权和最小,这就是最小生成树。
需要强调的是,最小生成树中的边数等于点数-1。

并查集

那么问题来了,我们怎么判断两个点是否相连呢?
这时候需要用到并查集。

大致思路

初始化每个点,将它们的父亲设为自己,当需要将两个点(或集合)合并时,只需要修改它们的父亲,使得一个点为这个集合中所有点的父亲,如果两个点父亲相同,就说明它们在一个集合中。

举例

举个便于理解的例子,有一群人,他们彼此不一定认识,那他们怎么区分彼此属于哪个团伙呢?A见到B只需要说自己的队长是XXX,B说自己的队长是XXX,如果队长相同,就是一个队的,当两个队需要合并时,选出一个大队长,再让两个队中的所有人认识大队长,这样遇到别人就可以报出大队长的名字,来确定是不是和对方属于一个队,这就是并查集。

kruskal克鲁斯卡尔

现在有了基础知识的积淀,我们来切入正题,kruskal算法又是如何求出一个图的最小生成树的呢?

算法简析

将所有的边按照他们的边权由小到大排序,然后进行一次遍历,枚举每一条边,如果边所连的两个点还没有被联通,就选取这条边来构造最小生成树,如果连通过,就不管了,继续枚举下一条边,直到枚举完成,所有选用的边就构成了最小生成树。

原理

因为边是由小到大进行排序的,所以先考虑的边一定比后考虑的边的边权要小,所以如果两个点有两种方式相连,先枚举到的方式一定比后枚举到的方式要优,所以不需要考虑链接两个点的多种方式中到底哪个是最优解。

图片说明

首先我们假设所有边都不存在,用虚线表示:
kruskal算法透彻理解(含并查集及最小生成树的解释)_第3张图片
然后我们按从小到大的顺序依次枚举每条边是否选取,第一条边两端的点不在一个集合中,所以连起来:
kruskal算法透彻理解(含并查集及最小生成树的解释)_第4张图片
然后依次把边权为3,4,5的4条边都选取(都符合选取条件)

kruskal算法透彻理解(含并查集及最小生成树的解释)_第5张图片
kruskal算法透彻理解(含并查集及最小生成树的解释)_第6张图片
kruskal算法透彻理解(含并查集及最小生成树的解释)_第7张图片
这时候我们来考虑边权为6的这条边,发现它两端的点已经被连在了一起,所以我们不选这条边权为6的边,用蓝色表示不选。
kruskal算法透彻理解(含并查集及最小生成树的解释)_第8张图片
同理,最后一条边也不选取
kruskal算法透彻理解(含并查集及最小生成树的解释)_第9张图片
这样我们就得到了原图的最小生成树。
其实kruskal就是并查集和贪心的结合,仔细看看图的话并不难理解。

伪代码
按边权排序;
for(int i=1;i<=边数;i++)
{
    if(两个点父亲相同) continue;
    else{
    选取的边[++cnt]=这条边;
    使两条边父亲相同;
    }
}

如果还不清楚的话就看代码吧

程序实现
//by floatiy
#include
#include
#include
using namespace std;
int n,cnt,num,ans,m;//n为点的数量,m是边的总数,num是已经选取了的边数 
int fa[105];//存储每个点的父亲 
struct Edge{
    int w;//边权 
    int to,pre;//边相连的两个点 
};
Edge len[1000];//结构体存储更方便调用 
bool cmp(Edge x,Edge y)//使sort按照边权大小排序 
{
    return x.w < y.w;
}
int find(int x)//并查集 找父亲的函数
//注意这里使用的是递归查找,可以让这个点(A)的父亲也认A的祖先做父亲,使得一次递归经过的所有点的父亲都变成祖先,大大降低了时间复杂度。 
{
    if(fa[x]==x) return x;
    return fa[x]=find(fa[x]);
}
void build(int x)//选取函数,答案加上边权值,然后两个点相连 
{
    ans+=len[x].w;
    fa[find(len[x].pre)]=find(len[x].to);
    return;
}
int main()
{
    int x,y,we;//两个点和边权 
    cin>>n>>m;//n个点 m个边  
    for(int i=1;i<=m;i++)
    {
        cin>>x>>y>>we;
        len[i].to=x;
        len[i].pre=y;
        len[i].w=we;
    }
    sort(len+1,len+m+1,cmp);//按边权排序 
    for(int i=1;i<=n;i++) fa[i]=i;//重点!必须事先把每个点的父亲赋值为它自己,不然默认每个点父亲都是0,会出错。 
    for(int i=1;i<=m;i++)//按边权从小到大枚举 
    {
        if(num==n-1) break;//如果选取的边足够了,就停止循环 
        if(find(len[i].pre)==find(len[i].to)) continue;//如果两个点的父亲相同(已经合并了),就不管它了 
        else{
            build(i);
            num++;//已经选取的边数+1 
        }
    }
    printf("%d",ans);
    return 0;
}

我们来看一下运行结果(以之前举例子的那张图作为数据)
kruskal算法透彻理解(含并查集及最小生成树的解释)_第10张图片

推荐题目

知识需要巩固,我推荐几道很适合初学者做的kruskal的裸题:
USACO 最短网络 Agri-Net (luogu1546)
局域网(luogu2820)
无线通讯网(luogu1991,此题略有难度)

你可能感兴趣的:(————图论————,Kruskal)