首先,这两个算法都是求最小生成树的算法。
首先,生成树是建立在无向图中的,对于有向图,则没有生成树的概念,所以接下来讨论的图均默认为无向图。对于一个有n个点的图,最少需要n-1条边使得这n个点联通,由这n-1条边组成的子图则称为原图的生成树。一般来说,一个图的生成树并不是唯一的(除非原图本身就是一棵树)。
在实际生活中我们常常会遇到这样一些问题:有若干个需要连接的点(不妨假设为一些村庄)和若干条连接着两个点的边(在村庄间修公路),而这些边会有不同的权值(可设为修路所需的费用不同)。现在要连通这些所有的点,并使权值和最小。这类问题在现实生活中很广泛,如修公路、架设电网,等等。
在信息学竞赛中,这种问题有专门的称谓“最小生成树”(Minimum Spanning Tree,简称MST)。
Kruskal算法:
顶点个数:n,边的条数:m
基本思想:
运用贪心的思想,每次选取权值最小的边,通过判断这条边所连接的两个顶点是否属于同一个集合,来决定这条边是否加入到生成树中去,如果不属于同一个集合,就加入到生成树中去,否则,就不加到生成树中去。直到有n-1条边加入到生成树中去(或者所有的边都访问完成了)就结束算法。
算法执行过程:
一:将边的所有权值按从小到大排序。
二:按照边的权值从小到大遍历所有的边。
三:每次遍历边的时候运用并查集判断边的两个顶点是否属于同一个集合,如果不属于同一个集合,那么就将这条边加入到生成树中去。
四:每次遍历边完成后,判断一下生成树中的边是否等于n-1,如果等于,直接输出结果,结束算法。如果不等于n-1,继续遍历其他的边。
代码如下:
#include<bits/stdc++.h>
#define MAXN 9999999
using namespace std;
int n,m;//n-->顶点个数 m-->边个数
int father[MAXN];//father[i]代表编号为i的顶点的父亲顶点的编号是多少
int sum=0;//加权总和
int n1=0;//最小生成树的边的条数
struct node
{
int u,v,w;
}a[MAXN];
bool cmp(node x1,node x2)//比较器
{
return x1.w<x2.w;
}
int getfather(int x)//找x节点的父亲节点的函数
{
if(father[x]==x)//自己就是自己的父亲节点
return x;
father[x]=getfather(father[x]);//递归寻找父亲节点,并且压缩路径
return father[x];//返回x节点的父亲节点的编号
}
void Kruskal()
{
for(int i=1;i<=n;i++)//初始化父亲节点数组
father[i]=i;
sort(a,a+m,cmp);//按照每个边的权值从小到大排序
for(int i=0;i<m;i++)//从边权值最小的边开始依次访问所有边
{
if(getfather(a[i].u)!=getfather(a[i].v))//这条边连接的两个顶点的父亲顶点不是同一个
{
sum+=a[i].w;
n1++;
father[a[i].v]=a[i].u;
}
if(n1==n-1)//由树的性质可知,树的边的条数是顶点数减一
{
cout<<sum<<endl;
return ;
}
}
return ;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
cin>>a[i].u>>a[i].v>>a[i].w;
Kruskal();
}
代码运行过程:
原图:
sum=0
第一步:
sum=1 m1=1<n-1
第二步:
sum=3 m1=2<n-1
第三步:
sum=6 m1=3<n-1
第四步:
sum=10 m1=4<n-1
第5步:
sum=19 m1=5=n-1 算法结束。所得的最小生成树是上图。
Prims算法:
基本思想:
还是运用贪心的思想,每次往生成树集合中加入离生成树距离最近的点,从一个点慢慢的扩展,最后得到所求的最小生成树。这个算法和求最短路的dijkstra算法很像。
算法执行过程:
n:顶点个数 m:边的条数
一:先选任意一个点加入到生成树集合中去。
二:遍历这个点的所有出边,更新出边点到生成树的最短距离。
三:进行n-1次遍历,每次遍历找到一个离生成树距离最近的非生成树的点,然后把该点加入到生成树集合中去,把该点标记为生成树的点。
四:重复三过程,这样,进行完n-1次遍历后,最小生成树就构造好了(每次遍历都往生成树中加入一个点,进行n-1次遍历后,所有顶点都已经加入到了生成树中去了),算法结束。
代码如下:
#include<bits/stdc++.h>
#define MAXN 9999
using namespace std;
int n,m;
int u,v,w;
int e[MAXN][MAXN];
int mincount[MAXN];//mincount[i]代表编号为i的顶点离生成树集合的最短距离(可以是生成树集合中的任意一点),和dijkstra算法中的dis数组类似
int visited[MAXN];//标记数组,标记i顶点是否进入了生成树集合
int sum=0;
void prims(int s)//先将s点放到生成树集合中去
{
memset(visited,0,sizeof(visited));
for(int i=1;i<=n;i++)//依次遍历所有的点,看一下哪个点和s点连通
if(e[s][i]<MAXN)//这个条件满足,说明s点和i点之间有边
mincount[i]=e[s][i];//更新mincount数组
visited[s]=1;//将s点标记
for(int i=1;i<n;i++)//进行n-1次循环,这样所有的点都能够访问到
{
int k;//找到在非树集合中离生成树集合距离最近的点
int minz=MAXN;
for(int j=1;j<=n;j++)
{
if(!visited[j]&&mincount[j]<minz)
{
minz=mincount[j];
k=j;
}
}
sum+=minz;
visited[k]=1;//将这个点加入到生成树集合中去,标记
for(int i=1;i<=n;i++)//依次遍历上面找到的点的所有出边,看看能不能通过这个点使得非树集合中的点到生成树集合的距离变短
mincount[i]=e[k][i]<mincount[i]?e[k][i]:mincount[i];
}
cout<<sum<<endl;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
e[i][j]=MAXN;
for(int i=0;i<m;i++)
{
cin>>u>>v>>w;
e[u][v]=w;
e[v][u]=w;
}
for(int i=1;i<=n;i++)//初始化mincount数组
mincount[i]=MAXN;
prims(1);
}
代码执行过程:
原图:
sum=0
第一步:
sum=1 第一次遍历
第二步:
sum=3 第二次遍历
第三步:
sum=12 第三次遍历
第四步:
sum=15 第四次遍历
第五步:
sum=19 第5次遍历 n-1等于5 算法结束 。
总结:
通过上面可得,Kruskal算法是一步步将森林中的树进行合并,而Prims算法是每次通过增加一条边来创建一棵树。Kruskal算法是宏观的建立一棵树,而Prims算法是从小开始,一步一步把树建立好。Kruskal算法适用于稀松图(边很少的图),因为Kruskal算法是通过边的权值建立生成树的,所以边的条数越少,算法就越快。而Prims算法适用于稠密图(边很多的图),因为Prims算法是通过顶点到生成树的距离来建树的,与边的条数没有多大的关系,所以适用于稠密图。