最小生成树之kruskal算法
Kruskal算法
1.概览
Kruskal算法是一种用来寻找最小生成树的算法,由Joseph Kruskal在1956年发表。用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪婪算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。
算法定义
2.算法简单描述
1).记Graph中有v个顶点,e个边
2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边
3).将原图Graph中所有e个边按权值从小到大排序
4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中
添加这条边到图Graphnew中
kruskal算法的精髓在于:
每次选取一条边。
该边同时满足:1、在当前未选边中权值最小;2、与已选边不构成回路。
直到选取n-1条表是算法结束。找到MST活判断不存在MST。
代码设计:
1、利用优先级队列将权值小的边放到队列最前,优先出对,保证了每次选择的都是权值最小的边。
2、利用并查集的查找及结合把同处同一连通分量中的顶点连到同一父节点下。这样,每次判断是
否构成回路,只要判断父节点是否相同的即可。
图例描述:
首先第一步,我们有一张图Graph,有若干点和边
将所有的边的长度排序,用排序的结果作为我们选择边的依据。这里再次体现了贪心算法的思想。资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。这样我们的图就变成了右图
在剩下的变中寻找。我们找到了CE。这里边的权重也是5
依次类推我们找到了6,7,7,即DF,AB,BE。
下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连)。所以不需要选择他们。类似的BD也已经连通了(这里上图的连通线用红色表示了)。
3.简单证明Kruskal算法
对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用。
归纳基础:
n=1,显然能够找到最小生成树。
归纳过程:
假设Kruskal算法对n≤k阶图适用,那么,在k+1阶图G中,我们把最短边的两个端点a和b做一个合并操作,即把u与v合为一个点v',把原来接在u和v的边都接到v'上去,这样就能够得到一个k阶图G'(u,v的合并是k+1少一条边),G'最小生成树T'可以用Kruskal算法得到。
我们证明T'+{<u,v>}是G的最小生成树。
用反证法,如果T'+{<u,v>}不是最小生成树,最小生成树是T,即W(T)<W(T'+{<u,v>})。显然T应该包含<u,v>,否则,可以用<u,v>加入到T中,形成一个环,删除环上原有的任意一条边,形成一棵更小权值的生成树。而T-{<u,v>},是G'的生成树。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),产生了矛盾。于是假设不成立,T'+{<u,v>}是G的最小生成树,Kruskal算法对k+1阶图也适用。
由数学归纳法,Kruskal算法得证。
综述:
Kruskal比较适用于稀疏图,是一种贪心算法:为使生成树上边的权值和最小,则应使生成树中每一条边的权值尽可能地小。
具体做法:找出森林中连接任意两棵树的所有边中,具有最小权值的边,如果将它加入生成树中不产生回路,则它就是生成树中的一条边。这里的关键就是如何判断"将它加入生成树中不产生回路"。
《算法导论》提供的一种方法是采用一种"不相交集合数据结构",也就是并查集了。具体的实现看代码好了,反正核心内容就是如果某两个节点属于同一棵树(Find_Set),那么将它们合并(Union)后一定会形成回路。
编写程序:对于如下一个带权无向图,给出所有边以及权值,用kruskal算法求最小生成树。
下面还是以sdutoj2144同样讲解此题:
题目描述:n个点,m条边,下面m行分别输入3个数据a,b,c分别代表a——>b的权值是c;
解决方案二: kruskal算法实现代码。
代码如下:详细的讲解已经在注释中:
第一种快速排序方式:
/*kruskal算法*/
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int bin[110];
int sum;
int num; //声明全局变量
struct node//创建结构体
{
int u,v,w;//存储边的信息
}q[100000];
int cmp(const void *a,const void *b)
{
return (*(node *)a).w-(*(node *)b).w;
}
int finds(int a)//查找根结点+路径压缩
{
if(a!=bin[a])
a=finds(bin[a]);
return a;
}
int main()
{
int n,m;
int i,j;
int a,b,c;//声明变量
while(~scanf("%d %d",&n,&m))//循环输入数据n和m的值
{
sum=0;//存储权值
num=0;//存储边数
for(i=1;i<=n;i++)
bin[i]=i; //初始化
for(i=0;i<m;i++)
{
scanf("%d %d %d",&q[i].u,&q[i].v,&q[i].w);
}
qsort(q,m,sizeof(q[0]),cmp);//按权值由小到大排序
for(i=0;i<m;i++)
{
int x=finds(q[i].u);
int y=finds(q[i].v);
if(x!=y)
{
sum+=q[i].w;//记录权值之和
bin[x]=y;//合并
num++;//边数加1
}
if(num==n-1)//最小生成树条件:边数=顶点数-1
{
break;
}
}
printf("%d\n",sum);
}
return 0;
}
第二种排序方式:操作运算法的使用
代码如下:
/*kruskal算法*/
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cstdio>
using namespace std;
int bin[110];
int sum;
int num; //声明全局变量
struct node//创建结构体
{
int u,v,w;//存储边的信息
bool operator <(const struct node &cmp)const
{
return w<cmp.w;
}
}q[100000];
int finds(int a)//查找根结点+路径压缩
{
if(a!=bin[a])
a=finds(bin[a]);
return a;
}
int main()
{
int n,m;
int i,j;
int a,b,c;//声明变量
while(~scanf("%d %d",&n,&m))//循环输入数据n和m的值
{
sum=0;//存储权值
num=0;//存储边数
for(i=1;i<=n;i++)
bin[i]=i; //初始化
for(i=0;i<m;i++)
{
scanf("%d %d %d",&q[i].u,&q[i].v,&q[i].w);
}
sort(q,q+m);//按权值由小到大排序
for(i=0;i<m;i++)
{
int x=finds(q[i].u);
int y=finds(q[i].v);
if(x!=y)
{
sum+=q[i].w;//记录权值之和
bin[x]=y;//合并
num++;//边数加1
}
if(num==n-1)//最小生成树条件:边数=顶点数-1
{
break;
}
}
printf("%d\n",sum);
}
return 0;
}