【图解算法】最小生成树

今天我们介绍图论中的另一部分,最小生成树。

对图的最短路感兴趣的同学可以看:
【图解算法】一次解决最短路径问题


1. 最小生成树简介

  • 最小生成树的概念:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树

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

举个例子:

我们余姚在n个城市之间铺设光缆,使它们之间都可以通信。但是铺设光缆的费用很高,且铺设光缆的费用与距离成正比,那么我们应该如何铺设光缆才能使总费用最低呢?

  • 常用的计算最小生成树的算法
  1. prim 算法
  2. kruskal算法

2. Prim算法

2.1 模板题

prim

2.2 思路模板

每次将距离已经连通部分最近的点 和对应的边 加入连通部分,是连通部分逐渐扩大,最后将整个图连接起来并且边长之和最小。

伪代码:

int dist[n],state[n],pre[n];
dist[1] = 0;
for(i : 1 ~ n)
{
    t <- 没有连通起来,但是距离连通部分最近的点;
    state[t] = 1;
    更新 dist 和 pre;
}

这样讲有点抽象,我们配合图举个例子:

首先,我们设置:

  1. state状态数组,表示节点是否已经再连通集当中,state[i] 为真,表示已经连通,state[i] 为假,表示还没有连通。初始时,state 各个元素为假。即所有点还没有连通
  2. dist 距离数组,保存各个点到连通集的距离(注意:不是到原点的距离),dist[i] 表示 i 节点到连通部分的最短距离。初始时,dist 数组的各个元素为无穷大
  3. pre 数组保存节点的是和谁连通的。pre[i] = k 表示节点 i 和节点 k 之间需要有一条边。初始时,pre 的各个元素置为 -1,这个数组视题目要求考虑要不要写。
    在这里插入图片描述

假定我们从1号节点开始扩充 连通集,所以 1 号节点与连通部分的最短距离为 0,即disti[1] 置为 0。

在这里插入图片描述

遍历 dist 数组,找到一个还没有连通起来,但是距离连通部分最近的点,假设该节点的编号是 i。i节点就是下一个应该加入连通部分的节点,stata[i] 置为 1。

如果我们用灰色表示尚未连通的点,绿色表示在连通集中的点,那么此时距离最小的显然是 1号节点,故state[1]置1.

在这里插入图片描述

接着,我们遍历节点1 的 所有可达点j,如果 j 距离连通部分的距离大于 i j 之间的距离,dist[j] > w[i][j](w[i][j] 为 i j 节点之间的距离),则更新 dist[j] 为 w[i][j]。这时候表示,j 到连通部分的最短方式是和 i 相连,因此,更新pre[j] = i。

与节点 1 相连的有 2, 3, 4 号节点。1->2 的距离为 100,小于 dist[2],dist[2] 更新为 100,pre[2] 更新为1。1->4 的距离为 140,小于 dist[4],dist[4] 更新为 140,pre[2] 更新为1。1->3 的距离为 150,小于 dist[3],dist[3] 更新为 150,pre[3] 更新为1。

在这里插入图片描述

我们之后,只需要不断重复上两步,直到所有节点都被加入了连通集:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
此时dist数组中保存的就是各个节点需要修的路,加起来就是总长度。pre数组中保存了需要选择的边
在这里插入图片描述


2.3 代码实现

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int N=510;
int  g[N][N];
bool st[N];//记录已经在集合中的点
int dist[N];//记录各个点到集合的距离
// int pre[N]; //该模板题没有要求输出所有边,所以不需要定义prev数组
int n,m;

int Prim()
{
    int ans=0;
    memset(dist,0x3f,sizeof(dist));
    dist[1]=0;
    for(int i=0;i<n;i++){
        int t=-1;
        //寻找距离连通集最近的点
        for(int j=1;j<=n;j++){
            if(!st[j]&&(t==-1||dist[j]<dist[t])){
                t=j;
            }
        }
        
        //判断当前节点是否与集合联通,不连通则说明没有最小生成树
        if(i && dist[t] == 0x3f3f3f3f) return 0x3f3f3f3f;
        
        ans+=dist[t];
        st[t]=true;
        for(int j=1;j<=n;j++){
            if(dist[j]>g[t][j]&&!st[j]){
                dist[j] = g[t][j];
                //pre[j] = t;//从 t 到 j 的距离更短,i 的前驱变为 t.
            }
          
        }
    }
    return ans;
}
//void getPath()//输出各个边
//{
//    for(int i = n; i > 1; i--)//n 个节点,所以有 n-1 条边。
//
//    {
//        cout << i <<" " << pre[i] << " "<< endl;// i 是节点编号,pre[i] 是 i 节点的前驱节//点。他们构成一条边。
//    }
//}

int main()
{
    cin>>n>>m;
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,w;
        cin>>a>>b>>w;
        g[a][b]=g[b][a]=min(g[a][b],w);
    }
    
    int t=Prim();
    if (t == 0x3f3f3f3f) puts("impossible");
    else cout<<t;
   
}

2.3 prim 算法的优化

与Dijkstra算法类似,Prim算法也可以使用堆来优化,优化之后时间复杂度由O(n^2)降为O(mlogN).适用于稀疏图,但是稀疏图的时候,其实使用 Kruskal算法更加实用。

所以,这里我们不讲解优化算法,感兴趣的同学可以参照Dijkstra算法的优化来实现。


3. Kruskal算法

3.1 模板题

在这里插入图片描述

3.2 思路模板

  1. 按照边的权值将边进行升序排序,然后从小到大一一判断
  2. 如果这个边与之前选择的所有边不会组成回路,就选择这条边,反之舍去
  3. 不断判断,知道具有n个顶点的联通网筛选出来n-1条边为止。

此时筛选出来的边和所有的顶点构成此连通网的最小生成树。


对于判断是否会产生回路,我们使用并查集。

不懂并查集的小伙伴可以看这位大佬的文章,简单易懂:
【算法与数据结构】—— 并查集

  • 在初始状态下 各个顶点在不同的集合中
  • 遍历每条边的时候,判断该边的两个顶点是否在一个集合中
  • 如果边上的这两个顶点在一个集合中,说明两个顶点一定已经连通,那么这条边加上去一定导致环,所以舍去该边。反之如果不在一个集合中,就加入这条边。

我们依然是画图来演示一下:

在这里插入图片描述
我们已经将边按照权值做了升序排列

  1. 选BD边,由于尚未选择任何边组成最小生成树,且 B-D 自身不会构成环路,所以 B-D 边可以组成最小生成树。

在这里插入图片描述
2. D-T 边不会和已选 B-D 边构成环路,可以组成最小生成树:
在这里插入图片描述

  1. A-C 边不会和已选 B-D、D-T 边构成环路,可以组成最小生成树:
    在这里插入图片描述

  2. C-D 边不会和已选 A-C、B-D、D-T 边构成环路,可以组成最小生成树:
    在这里插入图片描述

  3. C-B 边会和已选 C-D、B-D 边构成环路,因此不能组成最小生成树:

在这里插入图片描述

  1. B-T 、A-B、S-A 三条边都会和已选 A-C、C-D、B-D、D-T 构成环路,都不能组成最小生成树。而 S-A 不会和已选边构成环路,可以组成最小生成树。
    在这里插入图片描述

如图下图 所示,对于一个包含 6 个顶点的连通网,我们已经选择了 5 条边,这些边组成的生成树就是最小生成树。
在这里插入图片描述


3.3 代码实现

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 100010;
int p[N];//保存并查集

struct E{
    int a;
    int b;
    int w;
    bool operator < (const E& rhs){//通过边长进行排序
        return this->w < rhs.w;
    }

}edg[N * 2];
int res = 0;

int n, m;
int cnt = 0;
int find(int a){//并查集找祖宗
    if(p[a] != a) p[a] = find(p[a]);
    return p[a];
}
void klskr(){
    for(int i = 1; i <= m; i++)//依次尝试加入每条边
    {
        int pa = find(edg[i].a)// a 点所在的集合
        int pb = find(edg[i].b);// b 点所在的集合
        if(pa != pb){//如果 a b 不在一个集合中
            res += edg[i].w;//a b 之间这条边要
            p[pa] = pb;// 合并a b
            cnt ++; // 保留的边数量+1
        }
    }
}
int main()
{

    cin >> n >> m;
    for(int i = 1; i <= n; i++) p[i] = i;//初始化并查集
    for(int i = 1; i <= m; i++){//读入每条边
        int a, b , c;
        cin >> a >> b >>c;
        edg[i] = {a, b, c};
    }
    sort(edg + 1, edg + m + 1);//按边长排序
    klskr();
    if(cnt < n - 1) {//如果保留的边小于点数-1,则不能连通
        cout<< "impossible";
        return 0;
    }
    cout << res;
    return 0;
}
  • 20
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
prim算法是一种用于解决最小生成树(Minimum Spanning Tree,MST)问题的算法最小生成树是指在一个连通加权图中找到一棵包含所有顶点并且边权值之和最小的树。 下面以一个例题图来解释prim算法的过程。假设我们有一个加权图,顶点分别为A、B、C、D、E,边的权值为: AB: 2 AC: 3 AD: 7 BC: 8 BE: 4 CE: 5 DE: 6 首先选择一个任意顶点作为起始点,我们选择A点作为起始点。将A点标记为已访问,然后找到与A点相邻的边中权值最小的边,即AB,将B点标记为已访问。此时A—B这条边就成为了最小生成树的一部分。 接下来,我们需要找到与A、B点相邻的边中权值最小的边。分别是AC和BE,我们选择AC这条边,将C点标记为已访问。此时A—B和A—C这两条边就成为了最小生成树的一部分。 然后,我们找到与A、B、C点相邻的边中权值最小的边。分别是AD和CE,我们选择CE这条边,将E点标记为已访问。此时A—B、A—C和C—E这三条边就成为了最小生成树的一部分。 最后,我们找到与A、B、C、E点相邻的边中权值最小的边,即DE。将D点标记为已访问。此时A—B、A—C、C—E和D—E这四条边就组成了最小生成树。 通过上述过程,我们得到了最小生成树,其包含了ABCED这5个顶点,使得边的权值之和最小。这就是prim算法的过程,通过不断选择最小的边来构建最小生成树

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ornamrr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值