最小生成树模板——kruskal算法

kruskal算法

kruskal算法主要解决稀疏图,代替堆优化的prim算法。

假设 WN=(V,{E}) 是一个含有 n 个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:

先构造一个只含 n 个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有 n 棵树的一个森林。
之后,从网的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图
也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。kruskal算法限制每次选取的边不能使生成树构成环。
依次类推,直至森林中只有一棵树,也即子图中含有 n-1条边为止。

可以看出,kruskal算法和prim算法的不同之处,prim算法每次选取一个顶点,kruskal算法每次选取一条边。

依据kruskal算法的过程可知不需要像prim算法那样存储整个图的结构,只需要知道边的信息即可,

  1. 由于每次选取的是权值最小的边,所以可以根据边的权值从小到大排序,然后再遍历。
  2. 由于每次选取的边(a,b)的两个顶点要求不在一棵树上,因此需要并查集操作,判断a,b是否在一个集合。
      
     
      如果点a b在一个集合,会出现以下三种情况:
  1. a b是间接构成回路,有a->b的边时,如果插入就会构成回路;
  2. 重边:已有a->b的直接边,再次插入就没有必要,因为前面已经插入的权值必定小于后面
  3. 自环,肯定已经构成回路,必不可能插入。

因此每次需要判断两个点是否在一个连通图中

那么什么时候可以停止循环呢?

prim算法中循环N次可以停止,因为每次就会确定一个顶点;
但是kruskal算法不能够根据循环次数决定,因为不是每次循环的边都能加入集合,因此退出可以有两个条件:

  1. 已经统计到了n-1条边,可以退出循环
  2. 找到了边权为INF的边,此时说明存在不可达的点,是非连通图,不存在最小生成树,但是输入数据时并不会输入不存在的边,所以不存在会遍历到权值为INF的边,遍历的m条边都是可达的。

因此,循环提前退出时的条件只能是第1个,在遍历m条边之前就已经得到了最小生成树。
然后,当循环退出时,可能此时统计到了n-1条边;
也可能是非连通图,没有统计到n-1条边,如果没有统计到n-1条边,那就是不存在最小生成树。

最坏的情况下,全部遍历m条边才能停止。时间复杂度:O(mlogm)
因为遍历m条边过程中还涉及到并查集操作。

题目描述

给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。

求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。
由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

输入格式
第一行包含两个整数n和m。

接下来m行,每行包含三个整数u,v,w,表示点u和点v之间存在一条权值为w的边。

输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

数据范围
1≤n≤105,
1≤m≤2∗105,
图中涉及边的边权的绝对值均不超过1000。

输入样例:

4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4

输出样例:

6

算法实现

#include <iostream>
#include <algorithm>

using namespace std;

const int N=1e5+10,M=2e5+10; //只存储边的信息,不构造图,虽是无向图,只存条边就够了,a-b互通
struct Edge
{
    int a,b,w;
}edges[M];
int pre[N];//并查集
int n,m;

bool cmp(Edge x,Edge y)
{
    return x.w<y.w;
}

int find(int x)
{
    if(x!=pre[x]) pre[x]=find(pre[x]);
    return pre[x];
}

int kruskal()
{
    sort(edges,edges+m,cmp);
    for (int i=1;i<=n;i++) pre[i]=i;  //初始化并查集
    int num=0,res=0;//num记录生成树中已经加入的边数,res记录生成树的代价
    for (int i=0;i<m;i++) {
        int a=edges[i].a,b=edges[i].b,w=edges[i].w;
        a=find(a),b=find(b);
         //顶点a,b不在一个集合中时
        if (a!=b) pre[a]=b,res+=w,num++;
        if (num==n-1) return res;  //提前结束
    }
    return num==n-1?res:-1;
}

int main()
{
    scanf("%d%d",&n,&m);
    int a,b,c;
    for (int i=0;i<m;i++) {
        scanf("%d%d%d",&a,&b,&c);
        edges[i]={a,b,c};
    }
    int t=kruskal();
    t==-1?puts("impossible"):printf("%d",t);
    
    return 0;
}

kruskal算法的核心,每次选取的边的两个顶点不能在一个集合中。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值