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算法流程如下:
- 建立并查集,每个点各自构成一个集合。
- sort:把所有边按照权值从小到大排序,依次扫描每条边。
- 若x、y已经属于同一集合(连通),则忽略这条边。
- 若不属于同一集合,则合并x、y的集合,答案累计。
- 所有边扫描完成,就构成了最小生成树。
【二. 代码实现】
#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
——时间划过风的轨迹,那个少年,还在等你。