【暖*墟】 #图论# 最小生成树

1、最小生成树的概念

实际问题:在n个城市中建立一个通信网络,则至少需要布置n-1条通信线路。

这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?

于是我们就可以引入连通图来解决我们遇到的问题,n个城市就是图上的n个顶点,

边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,

所以现在我们有n个顶点的连通网可以建立不同的n-1条边的生成树。

当我们构造这个连通网所花的成本最小时,此图就称为最小生成树。

最小生成树的重要性质:MST性质

  • 假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,
  • 如果(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,
  • 则必定存在一颗包含边(u,v)的最小生成树。

下面就介绍两种使用MST性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

2、普里姆算法—Prim算法

【一. 算法思路】

V为点的全集。首先从图中的一个(任意)起点a开始,把a加入U集合,

从与a有关联的边中寻找权重最小的那条边、并且该边的终点b在顶点集合(V-U)中,

我们也把b加入到集合U中,并且输出边(a,b)的信息,

这样我们的集合U就有:{ a , b }。

寻找与a或b关联的边中、权重最小的那条、并且该边的终点在集合(V-U)中。

我们把c加入到集合U中,并且输出对应的那条边的信息,

这样我们的集合U就有:{a,b,c}这三个元素了,一次类推,直到所有顶点都加入到了集合U。

对下面这幅图求其最小生成树:

这里写图片描述

从顶点v1开始,(v1,v3)边的权重最小,所以第一个输出的边就是:v1—v3=1。

这里写图片描述

然后,我们要从v1和v3作为起点的边中寻找权重最小的边,

首先了(v1,v3)已经访问过了,所以我们从其他边中寻找,

发现(v3,v6)这条边最小,所以输出边就是:v3—-v6=4。

这里写图片描述

然后,我们要从v1、v3、v6这三个点相关联的边中寻找一条权重最小的边,

我们可以发现边(v6,v4)权重最小,所以输出边就是:v6—-v4=2。

这里写图片描述

然后,我们就从v1、v3、v6、v4这四个顶点相关联的边中寻找权重最小的边,

发现边(v3,v2)的权重最小,所以输出边:v3—–v2=5。

这里写图片描述

从v1、v3、v6、v4,v2这2五个顶点相关联的边中寻找权重最小的边,

发现边(v2,v5)的权重最小,所以输出边:v2—–v5=3。

这里写图片描述

最后,我们发现六个点都已经加入到集合U了,我们的最小生成树建立完成。

整个过程如图所示:
这里写图片描述

此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,

加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

【二. 代码实现】

#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<queue>
#include<algorithm>
#include<iostream>
using namespace std;
typedef long long LL;

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

//Prim算法【处理“点”】

int a[5019][5019],d[5019],n,m,ans=0; 
//↑↑↑把二维的距离数组a、在每次循r环中、判断转为一维的距离数组d
bool v[5019]; //vis数组标记点是否访问过

void prim(){
    memset(d,0x3f,sizeof(d)); //初始化为0x3f=1061109567
    memset(v,0,sizeof(v)); d[1]=0; //起点
    for(int i=1;i<n;i++){
        int x=0; //寻找最短边的编号
        for(int j=1;j<=n;j++)
            if(!v[j]&&(x==0||d[j]<d[x])) x=j;
        v[x]=1; //标记此时的最短边
        for(int y=1;y<=n;y++) //每次的边都是从x连向y
            if(!v[y]) d[y]=min(d[y],a[x][y]);
    }
}

int main(){
    reads(n); reads(m);
    memset(a,0x3f,sizeof(a)); //初始化为0x3f=1061109567
    for(int i=1;i<=n;i++) a[i][i]=0;
    for(int i=1;i<=m;i++){ //边和边权
        int x,y,z; cin>>x>>y>>z;
        a[y][x]=a[x][y]=min(a[x][y],z);
    }
    prim(); //求最小生成树
    for(int i = 1;i<=n;i++) ans+=d[i];
    cout<<ans<<endl; return 0;
}

3. 克鲁斯卡尔算法—Kruskal算法

【一. 算法思路】

(1)将图中的所有边都去掉。
(2)将边按权值从小到大的顺序添加到图中,保证添加的过程中不会形成环
(3)重复上一步直到连接所有顶点,此时就生成了最小生成树。这是一种贪心策略。

模拟克鲁斯卡算法生成最小生成树的详细的过程:
这里写图片描述

然后,我们需要从这些边中找出权重最小的那条边,

可以发现边(v1,v3)这条边的权重是最小的,所以我们输出边:v1—-v3=1。
这里写图片描述

然后,我们需要在剩余的边中,再次寻找一条权重最小的边,

可以发现边(v4,v6)这条边的权重最小,所以输出边:v4—v6=2。
这里写图片描述

再次寻找权重最小的边,发现边(v2,v5)的权重最小,输出边:v2—-v5=3。
这里写图片描述

寻找权重最小的边:(v3,v6),所以我们输出边:v3—-v6=4。
这里写图片描述

好了,现在我们还需要找出最后一条边就可以构造出一颗最小生成树,

但是此时有三个选择:(v1,V4),(v2,v3),(v3,v4),这三条边的权重都是5,

首先我们如果选(v1,v4)的话,得到的图如下:
这里写图片描述
我们发现,这肯定是不符合我们算法要求的,因为它出现了一个环,

所以我们再使用第二个(v2,v3)试试,得到图形如下:
这里写图片描述

我们发现,这个图中没有环出现,而且把所有的顶点都加入到了这颗树上了,

所以(v2,v3)就是我们所需要的边,所以最后一个输出的边就是:v2—-v3=5

此算法可以称为“加边法”,初始最小生成树边数为0,

每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

1. 把图中的所有边按代价从小到大排序;

2. 把图中的n个顶点看成独立的n棵树组成的森林;

3. 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,

    则成为最小生成树的一条边,并将这两颗树合并作为一颗树;

4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。

这里写图片描述

Kruskal算法流程如下:

  1. 建立并查集,每个点各自构成一个集合。
  2. sort:把所有边按照权值从小到大排序,依次扫描每条边。
  3. 若x、y已经属于同一集合(连通),则忽略这条边。
  4. 若不属于同一集合,则合并x、y的集合,答案累计。
  5. 所有边扫描完成,就构成了最小生成树。

【二. 代码实现】

#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<queue>
#include<algorithm>
#include<iostream>
using namespace std;
typedef long long LL;

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

//Kruskal算法【处理“边”】

struct node{
    int x,y,z;
}edge[200010];

int fa[200010],n,m,ans;

bool operator <(node a,node b){ return a.z<b.z; }

int find_fa(int x){
    if(x==fa[x]) return x;
    return fa[x]=find_fa(fa[x]);
}

int main(){
    reads(n); reads(m);
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].z);
    sort(edge+1,edge+m+1); //按照边权排序
    for(int i=1;i<=n;i++) fa[i]=i; //建立并查集
    for(int i=1;i<=m;i++){
        int fx=find_fa(edge[i].x);
        int fy=find_fa(edge[i].y);
        if(fx==fy) continue; //已经合并,就不用选择i这条边了
        fa[fx]=fy; ans+=edge[i].z;
    }
    cout<<ans<<endl; return 0;
}

详情可见 这里 qwq

                                               ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/82287119
今日推荐