克鲁斯卡尔算法(Kruskal)

克鲁斯卡尔算法和普利姆算法一样,用于构建最小生成树。普利姆算法基本思想就是寻找每个顶点权值最小的边,而克鲁斯卡尔算法则是依据边来寻找权值最小的边。

1. 算法过程

上图的邻接矩阵表示如下:

// 邻接矩阵
int edges[][vertex_size] = {
	{ 0,  7,  9,  M,  M, 14},
	{ 7,  0,  10, 15, M, M},
	{ 9,  10, 0,  11, M, 2},
	{ M,  15, 11, 0,  6, M},
	{ M,  M,  M,  6,  0, 9},
	{14,  M,  2,  M,  9, 0}
};

由于克鲁斯卡尔算法是基于边来构建最小生成树,所以第一步就要将我们构建的邻接矩阵,转换为边数组 edge_set,并对边数组根据 weight 升序排列:

begin     end       weight
3         6         2
4         5         6
1         2         7
1         3         9
5         6         9
2         3         10
3         4         11
1         6         14
2         4         15

另外初始化 parent_vertex 顶点数组全部为 0,该数组用于检查新的边是否和已经构建好的边产生环路。

{0, 0, 0, 0, 0, 0}

注意:边数组一定要根据 weight 升序排列,这会使得我们优先遍历低权重的边,优先将其添加到边结果中。

1. 第 1 步迭代

从 edge_set 取出第 1 个边:(begin, end, weight) = (3, 6, 2),借助 parent_vertex 数组,查看 begin 和 end 的最顶层顶点是否是相等的,如果两个顶点相等说明,新的边添加进来的话,会使得构建的最小生成树出现环路的问题。

顶点 3 和顶点 6 的位置为 0, 表示其没有父顶点。即:该边的加入不会导致环路。所以,我们设置 parent_vertex[3] = 6,并且打印 3->6 这条边,如下图所示:

// 假设索引从 1 开始
parent_vertex = {0, 0, 6, 0, 0, 0}

1.2 第 2 步迭代

重复前面的过程,从 edge_set 取出第 2 个边 (begin, end, weight) = (4, 5, 6):

  1. 顶点 4 的父顶点为 0,表示没有父顶点;
  2. 顶点 5 的父顶点为 0,表示没有父顶点;
  3. 4 不等于 5,表示该边的加入不会导致环路。

接下来设置 parent_vertex[4] = 5,表示该边已经存在,并且该边是有父顶点。此时如下图所示:

// 假设索引从 1 开始
parent_vertex = {0, 0, 6, 5, 0, 0}

1.3 第 3 步迭代

从 edge_set 取出第 3 个边 (begin, end, weight) = (1, 2, 7),计算每个顶点的祖宗顶点:

  1. 顶点 1 的父顶点为 0,表示没有父顶点;
  2. 顶点 2 的父顶点为 0,表示没有父顶点;
  3. 1 不等于 2,表示该边的加入不会导致环路。

接下来设置 parent_vertex[1] = 2,表示该边已经存在,并且该边是有父顶点。此时如下图所示:

// 假设索引从 1 开始
parent_vertex = {2, 0, 6, 5, 0, 0}

…依次类推,直到所有的边都遍历完毕,我就会得到一个最小生成树。如下图所示:

2. 算法实现

代码在实现过程中使用 STL 的 multiset 容器,该容器基于红黑树,内部能够自行排序,节省了自己的排序操作。

#include <iostream>
#include <set>
using namespace std;

#define M 65535

// 定义顶点
const int vertex_size = 6;
int vertex[] = { 1, 2, 3, 4, 5, 6 };

// 定义边
int edges[][vertex_size] = {
	{ 0,  7,  9,  M,  M, 14},
	{ 7,  0,  10, 15, M, M},
	{ 9,  10, 0,  11, M, 2},
	{ M,  15, 11, 0,  6, M},
	{ M,  M,  M,  6,  0, 9},
	{14,  M,  2,  M,  9, 0}
};


struct Edge
{
	Edge() {}
	Edge(int b, int e, int w) : begin(b), end(e), weight(w) {}

	bool operator<(const Edge &edge) const
	{
		return weight < edge.weight;
	}

	int begin;
	int end;
	int weight;
};


int get_origin_vertex(int vertex[], int current_vertex)
{
	// 假设当前顶点为起始顶点
	int origin_vertex = current_vertex;
	// 不断向上追溯其真正的起始顶点
	// 如果 vertext[origin_vertex] = 0 说明不再有父顶点
	while (vertex[origin_vertex] > 0)
	{
		origin_vertex = vertex[origin_vertex];
	}

	return origin_vertex;
}

void kruskal()
{	
	// 初始化边集,并将其根据 weight 升序排列
	multiset<Edge> edge_set;
	for (int i = 0; i < vertex_size; ++i)
	{
		for (int j = i + 1; j < vertex_size; ++j)
		{
			if (edges[i][j] != M)
			{	
				edge_set.insert(Edge(i, j, edges[i][j]));
			}
		}
	}

	// parent_vertex 用于记录顶点的父顶点,便于检查新的边是否会和已经构建好的边构成环路
	int parent_vertex[vertex_size] = { 0 };
	
	// 遍历所有的边
	for (auto &v : edge_set)
	{	

		// 获得当前边的开始顶点的祖宗顶点
		int origin_begin = get_origin_vertex(parent_vertex, v.begin);
		// 获得当前边的结束顶点的祖宗顶点
		int origin_end = get_origin_vertex(parent_vertex, v.end);

		if (origin_begin != origin_end)
		{
			// 进入此条件,说明新的边不会形成环路
			parent_vertex[origin_begin] = origin_end;
			// 打印新的边
			cout << v.begin << " " << v.end << endl;
		}
	}
}

int main()
{
	kruskal();
	return 0;
}

程序输出结果:

2 5
3 4
0 1
0 2
4 5

未经允许不得转载:一亩三分地 » 克鲁斯卡尔算法(Kruskal)