Kruskal算法(也称为克鲁斯卡尔算法)也是一种用来寻找最小生成树的算法 。与Prim算法不同的是,Prim算法是逐个加入权值最小边的顶点方法,而Kruskal算法则是对每条边按权值的递增次序选择合适的边。
问题:
G=(V,E)是一个具有n个顶点的带权连通无向图
T = (U,TE)是图G的最小生成树,其中U是T的顶点集合,TE是T的边集合
图1-图G
由图G构造最小生成树步骤:
1. 置U的初值等于V(也就是说,集合U中包含了图G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个分量) 。
2. 将图G中的边按权值从小到大的顺序依次选取:若选取的边未使生成树T形成回路,则加入TE;否则舍弃,直到TE中包含(n-1)条边为止。
1. Kruskal算法构造生成树过程
由上面的图G来选择权值最小的边构造最小生成树,过程如下:
图2
首先,我们通过查找图G发现,权值为1的边是最小的,于是我们就把(0,2)这条边加入进来。
图3
再次查找图G发现权值为2的这条边是最小的,于是我们再把(3,5)这条边加入进来。
图4
同理,把权值最小的(1,4)这条边加入进来,把(2,5)这条边加入进来。
图5
当再次从图G中搜索时发现,权值最小,且权值为5的边有三条:(0,3),(2,3),(1,2),但是(0,3)和(2,3)这两条边会出现回路问题,这是在构造最小生成树不允许出现的,因此(0,3)和(2,3)这两条边放弃加入,只剩下(1,2)这条边可以选择了,于是把(1,2)这条边加入进来,组成最小生成树。
所以在加入一条边的时候,还必须判断这条边是否会出现回路,如果出现回路那么就要放弃加入这条边,而这个判断的过程需要借助一个vset数组来完成 。
图6
我们假设图G是采用邻接矩阵方式存储的,再通过定义一个数组E来存储图G中的每一条边,按权值升序排列的方式存储边。
比如(0,2)这条边的权值是1,那么在数组E中存储就是n = 0,v = 2,w = 1。也就是说n代表(0,2)这条边的起始点,v是(0,2)这条边的终点,而w是(0,2)边的权值。其他的边和权值都是以这种方式来存储的。
当我们把图G中的边和权值全部都存储到数组E中,然后再按照权值升序进行排列方式对数组E中所有的元素进行排序就可以了
。
对应的数组E的存储结构定义如下:
//按权值升序排列的边
typedef struct
{
int u; //边的起始点
int v; //边的终点
int w; //边的权值
} Edge; //数组E
上面我们说到的这个数组E非常重要,需要存储图的边,并按次序排列,特别是当我们在构造生成树的时候,就需要从数组E中来选择合适的边;而数组vset同样也很重要,因为我们在选择加入一条边的时候,还需要判断是否会出现回路,这些是Kruskal算法需要解决的问题,那么下面我们来看一下Kruskal算法的实现。
2. Kruskal算法示例
图7
通过vset数组对每一个顶点进行编号,vset数组中元素的值就代表子图连通分量,用于标识子图是否属于同一连通分量。而在刚开始时,所有的顶点是没有边的,也就是说所有顶点都是属于不同的连通分量,所以刚开始vset数组中元素的值也是不同的,比如顶点0的连通分量是0,顶点1的连通分量是1。
因此我们从数组E中搜索权值最小的边,于是我们从数组E中搜索到(0,2)这条边,并把这条边加入到集合TE中组成生成树。把顶点0和顶点2连接起来后,那么顶点0和顶点2就是属于一个子图连通分量了,那么对应的在vset数组中会做一些相应的修改:在vset数组中把vset[2] = 2修改为0,表示顶点0和顶点2都是属于同一子图的连通分量0。
图8
同理,我们从数组E中搜索到(3,5)这条权值最小的边,把(3,5)这条边加入到集合TE中。并在vset数组中把vset[5] = 5修改为vset[5] = 3,表示顶点3和顶点5属于连通分量3。
图9
同理,把(1,4)这条边加入进来。在vset数组中修改vset[4]=4修改为vset[4]=1。
图10
同理,我们从数组E中搜索到(2,5)这条权值最小的边,把(2,5)这条边加入到集合TE中。此时我们发现,把连通分量0和连通分量3连接起来,合并成一个大的连通分量0了。由于(2,5)这条边中顶点2为起始点,顶点5为终点,所以把终点所在的连通分量合并为起点所在的连通分量
,因此在vset数组中把vset[5] 和vset[3]的值修改为0,表示顶点3和顶点5属于连通分量0。
图11
同理,当把(1,2)这条边加入进来后,连通分量1和连通分量0合并成一个大的连通分量1了。由于(1,2)这条边中顶点1是起始点,而顶点2是重点,所以把终点所在的连通分量合并为起始点所在的连通分量。因此在vset数组中把vset[0] ,vset[2],vset[3],vset[5]的值修改为1,表示顶点0,顶点2,顶点3,顶点5属于连通分量0。最后所有的顶点都属于一个连通分量。
此时集合TE = {(0,2),(3,5),(1,4),(2,5),(1,2)},最小生成树就构造完成了。
3. Kruskal算法实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAXV 6
#define MAXSIZE 100
#define INF 99
//图的定义:邻接矩阵
typedef struct MGRAPH{
int n; //顶点数
int e; //边数
int edges[MAXV][MAXV]; //邻接矩阵
} MGraph;
//按权值升序排列的边
typedef struct EDGE
{
int u;
int v;
int w;
} Edge;
//插入排序
void InsertSort(Edge E[],int n)
{
int i,j;
Edge temp;
for (i=1; i<n; i++)
{
temp=E[i];
j=i-1;
while (j>=0 && temp.w<E[j].w)
{
E[j+1]=E[j];
j--;
}
E[j+1]=temp;
}
}
void Kruskal(MGraph g)
{
int i,j,u1,v1,sn1,sn2,k;
int vset[MAXV];
Edge E[MAXSIZE];
//通过邻接矩阵来构造E数组并排序
k=0;
for (i=0; i<g.n; i++)
{
for (j=0; j<g.n; j++)
{
//权值非0,非无穷大
if (g.edges[i][j]!=0 && g.edges[i][j]!=INF)
{
E[k].u=i;
E[k].v=j;
E[k].w=g.edges[i][j];
k++;
}
}
}
//排序
InsertSort(E,g.e);
//开始初始化辅助数组vset
for (i=0; i<g.n; i++)
{
vset[i]=i;
}
//k从1开始,选出n-1条边
k=1; j=0;
while (k<g.n)
{
//起始点
u1=E[j].u;
//终点
v1=E[j].v;
//起始点的连通分量
sn1=vset[u1];
//终点的连通分量
sn2=vset[v1];
//是否为同一连通分量
if (sn1 != sn2)
{
//不是同一连通分量就输出边的信息
printf(" (%d,%d),权值:%d\n",u1,v1,E[j].w);
//k用于记录搜索的边数
k++;
//然后修改vset数组,把终点的连通分量合并为起始点的连通分量
for (i = 0; i < g.n; i++)
{
if (vset[i] == sn2)
{
vset[i] = sn1;
}
}
}
//j用于记录搜索的每个顶点
j++;
}
}
int main(void)
{
//用99数值代表无穷大
int A[MAXV][MAXV]=
{
{0,6,1,5,INF,INF},
{6,0,5,INF,3,INF},
{1,5,0,5,6,4},
{5,INF,5,0,INF,2},
{INF,3,6,INF,0,6},
{INF,INF,4,2,6,0}
};
int i;
int j;
//定义邻接矩阵存储结构
//另外,程序在优化的时候,是可以采用对称矩阵方式存储的
MGraph g;
//边数
g.e = 19;
//顶点数
g.n = 6;
for(i = 0; i < g.n; i++)
{
for(j = 0; j < g.n; j++)
{
g.edges[i][j] = A[i][j];
}
}
printf("\n");
printf("最小生成树:\n");
Kruskal(g);
printf("\n");
return 0;
}
测试结果: