最小生成树

一、简介

最小生成树就是将 n 个顶点, n - 1 条边,通过一个连接起来,且使权值最小的一种结构。

换句话来说,就是给定一个无向图,在图中选择若干条边把图中的所有节点连接起来,要求边长之和最小。在图论中,叫做求最小生成树。

主要的应用:
比如要在 n 个城市之间铺设光缆,使得这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,所以我们的目标要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

主要算法有 Prim算法Kruskal算法

下面通过一道Acwing上的一道最小生成树例题来分别简单介绍这两种算法。


最小生成树典例


二、Prim算法

可理解为 “加点法”, 每次迭代找到不在连通块中的距离最近的点,加入到连通块中,将连通块逐渐扩大,最后将整个图连通起来,并且边长之和最小。

1、先把所有点之间的距离初始化为正无穷

2、n次迭代,找到不在集合当中的最小的点,这个集合指当前已经在连通块中的所有点,找到该点赋给 t ,用 t 更新其他点到集合的距离,再把 t 加到集合当中去。

3、AC代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;

int n, m;//n 个点,m 条边
int g[N][N];//存储图
int dist[N];//存储各个点到生成树的距离
bool st[N];//判断点是否被加入到生成树中

//prim算法核心代码
int prim()
{
    //初始化距离数组为一个很大的数(10亿左右)
    memset(dist, 0x3f, sizeof(dist));
    
    int res = 0;//最小生成树所有边的长度之和
    
    //每次循环选出一个点加入到生成树
    for(int i = 0; i < n; i++){
    	//初始化为-1,表示没有找到任何一个点
        int t = -1;

        for(int j = 1; j <= n; j++){
        	//如果没有在树中,且到树的距离最短,则选择该点
            if(!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;//将该点赋给t
        }
        
        //一定要先累加,再进行更新生成树
        //遍历所有点,找不到连通的其他点,即不存在最小生成树
        if (i && dist[t] == INF)
            return INF;
            
        if (i)//把找到的符合条件的点的长度加上
            res += dist[t];
        
        //更新生成树,其他的点到生成树的最短距离
        for (int j = 1; j <= n; j++)
            dist[j] = min(dist[j], g[t][j]);
        
        //加入生成树中,标记
        st[t] = true;
    }
    return res;
}

int main()
{
    //各个点之间的距离初始化成无穷大
    memset(g, 0x3f, sizeof(g));
    
    cin >> n >> m;
    while(m --){
        int a, b, w;
        cin >> a >> b >> w;

		//存储两点之间最小的权重
        g[a][b] = g[b][a] = min(g[a][b], w);
    }

    int t = prim();
    
    //如果t为无穷大,则代表找不到符合条件的点,无法构成最小生成树
    if (t == INF)
        puts("impossible");
    else 
        printf("%d\n", t);
    
    return 0;
}

三、Kruskal算法

可理解为 “加边法”,最初最小生成树的边数为 0,每次迭代选择一条不在集合内的权值最短的边,加入到集合中,组成最小生成树。

1、使用快排将所有边按权值从小到大排序。时间复杂度为 O(log n).

2、枚举每组边 a 、b,权重 c ,如果 a、b不连通,就将这条边加入集合中,直到具有 n 个顶点的连通块筛选出来 n-1 条边为止。时间复杂度为 O(n) .

3、判断 a、b是否连通的方法为:使用并查集。

  • 初始化各个顶点在不同的集合中,父节点为它自己。
  • 按快排的从小到大的顺序遍历每条边,判断这条边的两个顶点是否有相同的父节点,如果有那就使在同一个集合中。
  • 如果该条边上的两个顶点在一个集合中,说明两个顶点已经连通,这条边不要。如果不在一个集合中,则加入这条边到集合中,连通这两个顶点。

总的时间复杂度为 O(n log n).

4、AC代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;

int n, m;
int p[N];

//结构体存储 两点及其权值
struct Edge
{
    int a, b, w;
    
    //重载小于号,因为再给边排序的时候是按照边的权重进行排序的,这样当两个边进行比较的时候就会使用他们的权重进行比较了
    bool operator < (const Edge &W)const
    {
        return w < W.w;
    }
}edges[N];

//查找根节点
int find(int x){
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    //初始化
    for (int i = 0; i < m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i] = {a, b, w};
    }
    
    //快排,将所有边的权值从小到大排序
    sort(edges, edges + m);
    
    //初始化并查集
    for (int i = 1; i <= n; i++)
        p[i] = i;
    
    后面这部分就是"Kruskal算法"的核心代码
    int res = 0;//res存的是最小生成树的所有边的权值
    int cnt = 0;//cnt存的是当前加入的边数
    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);
        if (a != b){
            p[a] = b;//将 a,b 所在的两个集合连接起来
            res += w;//集合中的总权重加上这条边的权重
            cnt++;//因为加入的是a-b这一条边,将a,b所在的两个集合连接之后,总的边数+1
        }
    }
    
    
    //只有当 cnt == n - 1 时才能表示已经将所有点加入到集合中,可以生成最小生成树
    if (cnt < n - 1)
        puts("impossible");
    else 
        printf("%d\n", res);
    
    return 0;
}

四、小结

1、点多边少,即稀疏图,一般选用 Prim算法, 采用 邻接矩阵 存储边之间的关系。
2、点少边多,即稠密图,一般选用 Kruskal算法,采用 邻接表 存储边之间的关系,但更简单更常用的是用 结构体 的方式来存储。

  • 11
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BraumAce

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值