最小生成树——Kruskal算法
1,问题描述
设G=(V,E)是无向连通带权图,如果G的一个子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。生成树的各边权的总和称为该生成树的耗费,求在G的所有生成树中耗费最小的最小生成树。
2,算法思想
(1)将代价树中权值非0的所有的边进行小顶堆排序,依次存入到road[]数组中,然后将road[]进行倒置,注意在进行排序时,按照road[i]的权值进行排序,然后记录这条边的起始顶点也要相对应。
(2)从最小边开始扫描各边,并检测当前所选边的加入是否会构成回路,如果不会构成回路,则将该边并入到最小生成树中。
(3)不断重复步骤2,直到所有的边都检测完为止。
其中判断当前检测边的加入是否会使原来生成树构成回路的算法思想是:
(1)采用树的双亲存储结构,即数组v[i]的值表示编号为i的顶点的父结点编号。并将数组v[]初始化为v[i]=i;
(2)若当前要并入的边起点为a,终点为b,需要判断起点a是否被修改过,即a!=v[a],若已被修改过,就要修改终点v[b]的值,使v[b]=a,即结点b的父结点为a。
(3)若当前检测的边结点起点为a,终点为b,则判断该边是否能被加入的方法是:分别访问a,b的根结点(a,b的父结点有可能还有父结点),若a,b的根结点相同,则不可以并入,否则可以将该边并入。
取得顶点a根结点的算法实现为:
//顶点a所在的连通分支号
int GetRoot(int a){
while(a!=v[a]){
a=v[a];
}
return a;
}
3程序设计
(1)所用数据结构,图的存储结构模块(nodetype.h)
#define maxSize 20
typedef struct{
int no;
}VertexType; //结点类型定义
typedef struct{
int n; //图的顶点数
int e; //图的所有边数
VertexType vex[maxSize];//顶点信息
int edges[maxSize][maxSize];//各边的权值
}MGraph; //图的存储结构
typedef struct{
int a;//边的起点
int b;//边的终点
int w;//边的权值
}Road; //权值非0的边的存储结构
(2)主模块(main_Kruskal.cpp)
#include
#include
#include"nodetype.h"
#include"initlize.h"
#include"roadinfor.h"
#include"heapSort.h"
#include"kruskal.h"
int main(){
int sum=0; //存储最小生成树的总代价
Initlize(g); //图信息的初始化,录入初始信息
Roadinfor(road,g.n); //录入边的信息
printf("最小生成树边的编号 起点 终点 权值\n");
Kruska(g,road,v,sum); //调用Kruskal算法,输出最小生成树
printf("\n最小代价生成树为: %d\n",sum);
return 0;
}
(3)读取数据模块,图的初始化(initlize.h)
//初始化图结构,录入图的信息
void Initlize(MGraph &g){
int i,j;
printf("请输入图的顶点数目:\n");
scanf("%d",&g.n); //读取顶点数目
printf("请输入各边的权值:(不相临接的两顶点权值为0)\n\n");
for(i=1;i<=g.n;i++){
g.vex[i].no=i;
for(j=1;j<=g.n;j++){
printf("边[%d][%d]的权值为:",i,j);
scanf("%d",&g.edges[i][j]);
printf("\n");
}
}
}
MGraph g; //图
Road road[maxSize]; //边的信息
int v[maxSize]; //顶点集
(4)读取各条候选边(待排序边)信息模块(roadinfor.h)
//读取各边的信息,并将之存入road[]中
void Roadinfor(Road road[],int e){
int i,j;
int k=1;
for(i=1;i<=e;i++){
for(j=i;j<=e;j++){
if(g.edges[i][j]!=0){
road[k].a=i;
road[k].b=j;
road[k].w=g.edges[i][j];
k++;
}
}
}
}
(5)将待检测边进行堆排序模块(heapSort.h)
int Sizearray(Road road[]); //求边的数目,即road[]数组元素的个数//R[low]到R[high]范围内对位置low到high结点进行调整
void Sift(Road road[],int low,int high);
//将边对象road2的信息赋值给边对象road1
void Switch(Road &road1,Road road2);
void verse(Road road[]); //将road数组倒置
void HeapSort(Road road[],int e){ //对边的权值进行堆排序,从小到大
int i;
Road temp;
for(i=(Sizearray(road))/2;i>=1;){
Sift(road,i,Sizearray(road));//建立初始小顶堆
i--;
}
for(i=(Sizearray(road));i>=2;i--){ //取得堆顶元素之后,不断对堆进行调准
Switch(temp,road[1]);
Switch(road[1],road[i]);
Switch(road[i],temp);
//只需要从新的堆顶开始调准,只有堆顶可能不满足小顶堆条件
Sift(road,1,i-1);
}
}
void Sift(Road road[],int low,int high){
int i,j;
i=low;
j=2*i;
Road temp;
Switch(temp,road[i]); //先将堆顶存入temp
while(j<=high){
if(jroad[j+1].w)//找到其最小的儿子
j++;
if(temp.w>road[j].w){ //若不满足小顶堆条件,则需进行调准
Switch(road[i],road[j]);
i=j;
j=2*i;
}
else{
break;
}
}
Switch(road[i],temp); //最后确定road[i]的位置
}
int Sizearray(Road road[]){
int i,n=0;
for(i=1;road[i].w!=0;i++){
n++;
}
return n;
}
void Switch(Road &road1,Road road2){
road1.a=road2.a; //road1起点赋值
road1.b=road2.b; //road1终点赋值
road1.w=road2.w; //road1权值赋值
}
void verse(Road road[]){
int i,n;
Road temp;
n=Sizearray(road);
for(i=1;i<=n/2;){
Switch(temp,road[i]);
Switch(road[i],road[n-i+1]);
Switch(road[n-i+1],temp);
i++;
}
}
(6)运用贪心策略——Kruskal算法构造最小生成树(kruskal.h)
int GetRoot(int a); //取得顶点的根节点,从而得到连通分支
//Kruskal算法,实现求最小生成树
void Kruska(MGraph &g,Road road[],int v[],int &sum){
int i,x,y;
int a,b;
//顶点ver[i]初始时表示各在不同的连通分支v[i]中,父结点依次为v[i]
for(i=1;i<=g.n;i++){
v[i]=i;
}
//堆排序,将边按照从小到大的顺序排序
HeapSort(road,g.e);
verse(road);
for(i=1;i<=Sizearray(road);i++){
//得到起点所在的连通分支号
x=GetRoot(road[i].a);
//得到终点所在的连通分支号
y=GetRoot(road[i].b);
//添加road[i]不会成环,将边road[i]添加其中
if(x!=y){
printf(" %d %d %d %d\n",
i,road[i].a,road[i].b,road[i].w);
if(x!=road[i].a){
//road[i]的起点已经被修改过,这时需要修改边road[i]的终点的连通分支号
v[road[i].b]=road[i].a;
sum+=road[i].w;
}else{
//road[i]的起点未被修改过,这时修改边road[i]的起点的连通分支号
v[road[i].a]=road[i].b;
sum+=road[i].w;
}
}
}
}
//顶点a所在的连通分支号
int GetRoot(int a){
while(a!=v[a]){
a=v[a];
}
return a;
}
6,实验总结
通过实现求最小生成树的两种算法,Prim算法和Kruskal算法,一个从顶点集考虑,一个从边集考虑,分别实现了构造最小生成树,当然Prim从单个顶点出发,利用贪心策略,开始顶点不同,可能构造的最小生成树可能不同,但最终的总耗费却是唯一的。同时思考问题的角度不同,带来执行算法效率的不同,Prim的时间复杂度为O(n2)(n为图的顶点数),与图的顶点数有关,边数无关,故适合顶点数目稀少的图,即密集图,而Kruskal的时间复杂度为O(eloge)(e为图中边的数目),与图的边数有关,故适合边数较少的稀疏图。