五、最小生成树——克鲁斯卡尔(Kruskal)算法

        现在我们来换一种思考方式,普里姆(Prim)算法是以某顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树的。这就像是我们如果去参观某个展会,例如世博会,你从一个入口进去,然后找你所在位置周边的场馆中你最感兴趣的场馆观光,看完后再用同样的办法看下一个。可我们为什么不事先计划好,进园后直接到你最想去的场馆观看呢?

        同样的思路,我们也可以直接就以边为目标去构建,因为权值是在边上,直接去找最小权值的边来构建生成树也是很自然的想法,只不过构建时要考虑是否会形成环路而已。此时我们就用到了图的存储结构中的边集数组结构。以下是edge边集数组结构的定义代码:

/*边集数组Edge结构*/
typedef struct
{
	int begin;
	int end;
	int weight;
}Edge;

        边集数组是由二维数组构成。这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成,如下图所示。

五、最小生成树——克鲁斯卡尔(Kruskal)算法_第1张图片

         其中begin是存储起点下标,end是存储终点下标,weight是存储权值。 

下图为某个带权无向图以及它的边集数组

五、最小生成树——克鲁斯卡尔(Kruskal)算法_第2张图片

可以发现这个边集数组是按weight(权值)来排序的。 

具体的实现方式如下:

#include
#include
#include
using namespace std;

#define MAXVEX 100//最大顶点数
typedef char VertexType;//顶点类型
typedef int EdgeType;//边上的权值类型
typedef struct
{
	VertexType vexs[MAXVEX];//顶点表
	EdgeType arc[MAXVEX][MAXVEX];//邻接矩阵
	int numVertexte;//当前顶点数
	int numEdges;//当前边数
}MGraph;

/*边集数组Edge结构*/
typedef struct
{
	int begin;
	int end;
	int weight;
}Edge;

void MiniSpanTree_Kruskal(MGraph G)
{
	vector edges(G.numVertexte);//定义边集数组
	vector parents(G.numVertexte);//该数组用来判断边与边是否形成环路

	/*初始化边集数组*/
	for (int i = 0; i < G.numVertexte; ++i)
	{
		for (int j = 0; j < G.numVertexte; ++j)
		{
			if (G.arc[i][j] != 0 && G.arc[i][j] != INT_MAX)
			{
				edges.push_back({ i,j,G.arc[i][j] });
				G.arc[j][i] = INT_MAX;
			}
		}
	}

	sort(edges.begin(), edges.end(), [&](const Edge& a, const Edge& b)
		{
			return a.weight <= b.weight;
		});//让边集数组按照权值来从小到大排序

	for (int i = 0; i < G.numVertexte; ++i)
	{
		parents[i] = 0;
	}

	for (int i = 0; i < G.numVertexte; ++i)//循环每一条边
	{
		int n = Find(parents, edges[i].begin);
		int m = Find(parents, edges[i].end);
		if (m != n)//如果m != n,说明此边没有和现有生成树形成环路
		{
			parents[n] = m;
			cout << edges[i].begin << edges[i].end << edges[i].weight << endl;
		}
	}
}

int Find(vector& parents, int f)//寻找连线顶点的尾部下标
{
	while (parents[f] > 0)
	{
		f = parents[f];
	}
	return f;
}

最终的结果如下图所示: 

五、最小生成树——克鲁斯卡尔(Kruskal)算法_第3张图片

 现在我们着重来看一下数组parents的含义:

  1. 当i = 0时,计算得知n = 4,m = 7,且m != n,那么我们将(v4,v7)纳入最小生成树中,此时的parents数组为{0,0,0,0,7,0,0,0,0}。
  2. 当i = 1时,计算得知n = 2,m = 8,且m != n,那么我们将(v2,v8)纳入最小生成树中,此时的parents数组为{0,0,8,0,7,0,0,0,0}。
  3. 当i = 2时,计算得知n = 0,m = 1,且m != n,那么我们将(v0,v1)纳入最小生成树中,此时的parents数组为{1,0,8,0,7,0,0,0,0}。
  4. 当i = 3时,计算得知n = 0,m = 5,且m != n,那么我们将(v0,v5)纳入最小生成树中,但我们发现此时已经有了parents[0] = 1,即v0->v1了,那么在想要从v0->v5,就要途径v1了,即变为了v0->v1->v5,此时的parents数组为{1,5,8,0,7,0,0,0,0}。
  5. 当i = 4时,计算得知n = 1,m = 8,且m != n,那么我们将(v1,v8)纳入最小生成树中,但我们发现此时已经有了parents[1] = 5,即v1->v5了,那么在想要从v1->v8,就要途径v5了,即变为了v1->v5->v8,此时的parents数组为{1,5,8,0,7,8,0,0,0}。
  6. 当i = 5时,计算得知n = 3,m = 7,且m != n,那么我们将(v3,v7)纳入最小生成树中,此时的parents数组为{1,5,8,7,7,8,0,0,0}
  7. 当i = 6时,计算得知n = 1,m = 6,且m != n,那么我们将(v1,v6)纳入最小生成树中,但我们发现此时已经有了parents[1] = 5,即v1->v5了,那么在想要从v1->v6,就要途径v5和v8了,即变为了v1->v5->v8->v6,此时的parents数组为{1,5,8,7,7,8,0,0,6}。
  8. 当i=7时,会得到m = n的结果,即会形成环,所以跳过这次循环。
  9. 当i=8时,会得到m = n的结果,即会形成环,所以跳过这次循环。
  10. 当i = 9时,计算得知n = 6,m = 7,且m != n,那么我们将(v6,v7)纳入最小生成树中,此时的parents数组为{1,5,8,7,7,8,7,0,6}。
  11. 此后边的循环均造成环路,我们不再讨论。

        如果不考虑初始化边集数组,和将边集数组按照权值排序的时间复杂度,那么此算法的Find函数由边数e决定,时间复杂度为O(log e),而外面有一个for循环e次。所以克鲁斯卡尔算法的时间复杂度为O(elog e)。

        克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。

你可能感兴趣的:(图,最小生成树,克鲁斯卡尔,图,Kruskal)