前面一篇博客讲的prim算法最小生成树之普里姆算法是以某一个顶底为起点,逐步找个顶点上权值最小的边来构建最小生成树的,既然我们的权值在边上,那么我们为何不根据边 来构建最小生成树呢?这就是克鲁斯卡尔算法。老规矩,先上代码再讲解:
typedef char VertexType; //顶点类型,可根据需要修改
typedef int EdgeType; //边上的权值类型,可根据需要修改
#define MAXVEX 100 //最大定点数,应由实际情况定
#define MAXEDGE 15 //边数量的最大值,根据实际情况而定,这里定的15和后面讲的例子有关
#define INFINITY 65535 //代表∞
typedef struct
{
char vexs[100]; //定点表
int arc[100][100]; //邻接矩阵,可看做边表
int numVertexes, numEdges; //图中当前的顶点数和边数
} MGraph;
typedef struct //边集数组
{
int begin; //起点下标
int end; //终点下标
int weight; //权值
}Edge
/* 交换权值 以及头和尾 */
void Swapn(Edge *edges,int i, int j)
{
int temp;
temp = edges[i].begin; //交换起点下标
edges[i].begin = edges[j].begin;
edges[j].begin = temp;
temp = edges[i].end; //交换终点下标
edges[i].end = edges[j].end;
edges[j].end = temp;
temp = edges[i].weight; //交换权值
edges[i].weight = edges[j].weight;
edges[j].weight = temp;
}
/* 对权值进行排序 */
void sort(Edge edges[],MGraph *G)
{
int i, j;
for ( i = 0; i < G->numEdges; i++)
{
for ( j = i + 1; j < G->numEdges; j++)
{
if (edges[i].weight > edges[j].weight)
{
Swapn(edges, i, j);
}
}
}
}
/*查找连线定点的尾部下标*/
int Find(int *parent, int f)
{
while(parent[f] > 0)
f = parent[f];
return f;
}
/*克鲁斯卡尔算法*/
void MiniSpanTree_Kruskal(MGraph G)
{
int i, n, m;
Edge edges[MAXEDGE];
int parent[MAXVEX];
for ( i = 0; i < G.numVertexes-1; i++) //用邻接矩阵来构建边集数组并排序
{
for (j = i + 1; j < G.numVertexes; j++)
{
if (G.arc[i][j]<INFINITY)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sort(edges, &G);
for (i = 0; i < G.numVertexes; i++)
parent[i] = 0; // 初始化数组值为0
printf("打印最小生成树:\n");
for (i = 0; i < G.numEdges; i++) // 循环每一条边
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) // 假如n与m不等,说明此边没有与现有的生成树形成环路
{
parent[n] = m; // 将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
}
代码有点长,不过没关系,不怕,慢慢走就行了。
咱们的主菜是克鲁斯卡尔算法,所以直接来看MiniSpanTree_Kruskal()函数。首先是一堆定义,记住parent[]数组,后面要考的。咱们遇到的第一个是两个for循环的嵌套,其目的是将我们的邻接矩阵转换成边集数组。先存进edges对应的项中,等存完后(几条边就几项)就排序,再来看看sort()函数,是根据权值的大小进行排序的,交换的时候用到Swapn()函数,没事儿,进去看看。进去后发现,原来就是每项都交换一下,我们仍然以我上一篇博客里的图为例,edges[]数组如下:
继续向后走,碰到一个用来初始化parent数组的for循环,初始化parent[]所有成员为0,其作用到后面会详细说,继续向下走,野生的for循环又出现了。先是算n,跳到Find()函数去执行,由于所有的parent[]都是0,所以直接返回了edges[0].begin = 4,所以n = 4,同理,m = 7。一看m != n啊,执行if里的内容,将终点下标赋值给下标为起点的parent[]中,打印(4,7)7。
for (i = 0; i < G.numEdges; i++) //循环每一条边
{
n = Find(parent,edges[i].begin);
m = Find(parent,edges[i].end);
if (n != m) // 假如n与m不等,说明此边没有与现有的生成树形成环路
{
parent[n] = m; // 将此边的结尾顶点放入下标为起点的parent中。表示此点已经在生成树集合中
printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight);
}
}
此时的parent[] = {0, 0, 0, 0, 7, 0, 0, 0, 0};
我们已经将边(V4, V7)放入最小生成树中了(如下图左):
接着循环执行上述步骤,得到打印结果(2, 8)8,parent[2] = 8;即(V2, V8)也被树纳入其中
此时parent[] = {0, 0, 8, 0, 7, 0, 0, 0, 0}(上图右)
再来,得到(0, 1)10,parent[0] = 1;即(0, 1)也被放进树中
此时parent[] = {1, 0, 8, 0, 7, 0, 0, 0, 0}
这是你该发现了,每次都是寻找的剩下的所有边中寻找权值最小的那个,一直执行到最后,打印的如下所示:(v0,v5)11,(v1, v8)12, (v3,v7)16,(v1, v6)16。
同时,parent[] = {1, 5, 8, 7, 7, 8, 0, 0, 6};具体变换过程如下图:
可以看到此时的网就被分割成两个连通的边集合了,记做A和B(上图网6)。现在来看看parent[]数组的意义,当parent[0] = 1时,表示v0和v1已经在生成树集合A中,此时将parent[0] = 1中的1改为下标,即查找parent[1],一看是5,表示v1和v5是在一个生成树集合中(也就是A中)。如此往复,parent[5] = 8,parent[8] = 6, parent[6] = 0,集合A到此为止,至此A中已经有v0,v1,v5,v8,v6。再查看parent[]中没有查看的值,发现其中的parent[2] = 8,表示v2和v8是在同一个生成树集合中,因此在A中再加上v2。parent[3] = 7, parent[4] = 7,parent[7] = 0表示v3,v4,v7是在一个生成树集合中,我们记做是B,这样的两个集合可不行,我们最后要的是一个,也就是说parent[]数组里只能有一个0,表示所有的点都连通了,如果没有0不就是一个环了吗。
讲完了parent[]的作用,我们继续最后一步,此时的i已经运行到7了,我们进去看看是什么,edges[7].begin = 5,进入Find(),parent[5] = 8, parent[8] = 6, parent[6] = 0,所以返回6,即n = 6,再看m,传入edges[7].end = 6,进入Find(),parent[6] = 0,返回6,m = 6,此时m = n,也就是告诉我们不能讲v5和v6之间的边加进去,不然就形成环路了,继续下一波循环,i = 8。
直接看edgs[8]的begin和end,看到是v1和v2,也不能连,理由同上,没事,继续循环,i = 9。
将edges[9].begin = 6传入Find(),parent[6] = 0,所以n = 6;将edges[9].end = 7传入Find(),parent[7] = 0,n = 7,好了最后一条路找到了,打印出来(v6,v7)19,此时parent[] = {1, 5, 8, 7, 7, 8, 7, 0, 6}只剩一个parent[]是0,不管怎么循环,下面的都只能是m = n = 7。至此就结束了:
克鲁斯卡尔针对的是边,每次都是寻找最短的,这也是贪心的一个例子,其时间复杂度为O(nlogn),由于是针对边,所以比较适用于稀疏图,即点多边少的情况