1.最小生成树
我们在讲图的定义和术语时,曾经提到过,一个连通图的生成树是一个极小的连通子图,它含有图中全部的顶点,但只有足以构成一棵树的n-1条边。那么我们把构造连通网的最小代价生成树称为最小生成树。在了解最小生成树的概念之后,我们更应该看重的是如何获取最小生成树,并如何利用最小生成树来解决问题。
2.普利姆算法(Prim)
在给出Prim算法代码之前,我们先来演示一个实例,看看普利姆算法是如何一步步获取最小生成树的,如图1所示。 我们先来看下演示图,再和大家逐步分析:
主要思想,在当前可选路中选出最短的那条路。我们的目的是在不成环的前提下,连通所有结点,而不是一步遍历所有结点,所以当我们获取下一条路径时,我们的出发点可以是已走路径上的任意一点,下面给出最小生成树的生成步骤。
- 第一步:假设从V0点开始,与V0点相连的边有V0->V1=10,V0->V5=11,我们选择最短的那条边,即选择V0->V1。
- 第二步:这时我们的路径上有V0、V1两个顶点,与已走路径相连的边有V0->V5=11、V1->V6=16、V1->V8=12、V1->V2=18,我们选择最短的那条边,即选择V0->V5。
- 第三步:这时我们的路径上有V0、V1、V5三个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V1->V8=12、V1->V6=16,我们选择最短的那条边,即选择V1->V8。
- 第四步:这时我们的路径上有V0、V1、V5、V8四个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V1->V6=16、V8->V2=8、V8->V3=21,我们选择最短的那条边,即选择V8->V2。
- 第五步:这时我们的路径上有V0、V1、V5、V8、V2五个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V1->V6=16、V8->V3=21,V2->V3=22,我们选择最短的那条边,即选择V1->V6。
- 第六步:这时我们的路径上有V0、V1、V5、V8、V2、V6六个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V8->V3=21,V2->V3=22,V6->V3=24、V6->V7=19我们选择最短的那条边,我们选择最短的那条边,即选择V6->V5,但是我们发现,如果选这条边那么已走路径V0、V1、V6、V5构成的路径成环,所以排除该边,在剩余边中找最短边,即选择V6->V7。
- 第七步:这时我们的路径上有V0、V1、V5、V8、V2、V6、V7七个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V8->V3=21,V2->V3=22,V6->V3=24、V7->V3=16、V7->V4=7,我们选择最短的那条边,即选择V7->V4。
- 第八步:这时我们的路径上有V0、V1、V5、V8、V2、V6、V7、V4八个顶点,与已走路径相连的边有V5->V6=17、V5->V4=26、V1->V2=18、V8->V3=21,V2->V3=22,V6->V3=24、V7->V3=16、V4->V3=20我们选择最短的那条边,即选择V7->V3。
- 第九步:这时我们的路径上有V0、V1、V5、V8、V2、V6、V7、V4、V3九个顶点,所有顶点连接完成,结束。
现在我们来分析如何用代码来实现上述过程,这里我们用邻接矩阵来存储图中信息。为了防止重复经过某个点,所以我们定义一个类似visited数组。除此之外,还要存储与已走路径连接的边值,选出最小边,将最小边另一端的点加入已走路径中。大致思路是这样,下面给出普利姆算法代码,并加以说明。
//普利姆算法生成最小生成树
void MiniSpanTree_Prim(MGraph G)
{
int min,i,j,k;
int adjvex[MAXVEX];//保存相关顶点下标
int lowcost[MAXVEX];//保存相关顶点间边的权值
lowcost[0]=0;//初始化第一个权值为0,即V0加入生成树
//lowcost的值为0,在这里就是此下标的顶点已经加入生成树
adjvex[0]=0;//初始化第一个顶点下标为0
for(i=1;i<G.numVertexes;i++)//循环除下标为0外的全部顶点
{
lowcost[i]=G.arc[0][i];//将V0顶点与之有边的权值存入数组
adjvex[i]=0;//初始化都为v0的下标
}
for(i=1;i<G.numVertexes;i++)
{
min=INFINITY;//初始化最小权值为无穷大
//通常设置为不可能的大数字如32767,65535等
j=1;k=0;
while(j<G.numVertexes)
{
if(lowcost[j]!=0&&lowcost[j]<min)
{
//如果权值不为0,且权值小于min
min=lowcost[j];//则让当前权值成为最小值
k=j;//将当前最小值的下标存入k
}
j++;
}
cout<<adjvex[k]<<","<<k<<endl;//打印当前顶点边中权值最小边
lowcost[k]=0;//将当前顶点的权值设置为0,表示此顶点已经完成任务
for(j=1;j<G.numVertexes;j++)
{
if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j])
{
//若下标为k点点各边权值小于此前这些顶点未被加入生成树权值
lowcost[j]=G.arc[k][j];//将较小权值存入lowcost
adjvex[j]=k;//将下标为k的顶点存入adjvex
}
}
}
}
3.克鲁斯卡尔算法(Kruskal)
现在我们来换一种思考方式,普利姆算法是以某顶点为起点,逐步找顶点上最小权值的边来构建最小生成树的。而克鲁斯卡尔算法直接以边为目标去构建,因为权值是在边上,直接去找最小权值的边构建生成树也是很自然的想法,只不过构建时要考虑是否形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。以下是edge边集数组结构的定义代码:
//对边集数组Edge结构的定义
typedef struct{
int begin;
int end;
int weight;
}Edge;
我们将图2转化为图3的边集数组,并且对它们按权值从小到大排序。
克鲁斯卡尔算法如果通俗点介绍最小生成树的生成过程的话,就是:肉眼可见的依次挑最小边,如果成环,则跳过该边,直到最后所有点连接完毕,下面给出算法代码。
//Kruskal算法生成最小生成树
void MiniSpanTree_Kruskal(MGraph G)//生成最小生成树
{
int i,n,m;
Edge edges[MAXEDGE];//定义边集数组
int parent[MAXVEX];//定义一数组用来判断边与边是否形成环路
//此处省略将邻接矩阵G转化为边集数组edges并按权由小到大排序的代码
for(i=0;i<G.numVertexes;i++)
{
parent[i]=0;//初始化数组值为0
}
for(i=0;i<G.numEdges[i];i++)
{
n=Find(parent,edges[i].begin);
m=Find(parent,edges[i].end);
if(n!=m)//假如n与m不等,说明此边没有与现有生成树形成环路
{
parent[n]=m;//将次边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中
cout<<edges[i].begin<<","<<edges[i].end<<":"<<edges[i].weight<<endl;
}
}
}
int Find(int *parent,int f)//查找连线顶点的尾部下标
{
while(parent[f]>0)
f=parent[f];
return f;
}
4.总结
对比两个算法,克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普利姆算法对于稠密图,即边数非常多的情况会更好一些。