最小生成树 —— Kruskal 克鲁斯卡尔算法

目录

一、Kruskal 算法简介

1.1 Kruskal 算法思想

1.2 Kruskal 过程图解

1.3 Kruskal 两个核心问题

1.4 适用场景(Kruskal 与 Prime 的比较)

二、Kruskal 的实现

2.1 头文件

2.2 函数文件

2.3 主函数(数据测试)


一、Kruskal 算法简介

1.1 Kruskal 算法思想

Kruskal(克鲁斯卡尔)算法是一种用来在加权连通图中寻找最小生成树的算法,其操作对象是

Kruskal 思想

        1. 从加权图中找出所有的边;初始时,所有边都不属于最小生成树,最小生成树为\o\phi

        2. 从不属于最小生成树的边中找到权值最小的边,判断最小边及其连接的两个顶点加入到最小生成树是否会形成环路

         a)若不形成环路,则将此最小边及其连接的顶点并入最小生成树;

         b)若形成环路,则永远不再看此边,然后从剩下的且不属于最小生成树的边中,寻找权值最小的边。

        3. 重复上述步骤,直至所有顶点均连接在一起,并没有形成环路时,最小生成树就找到了。

1.2 Kruskal 过程图解

举例:

 

  

 

 

1.3 Kruskal 两个核心问题

问题1  如何寻找权值最小的边

        应对策略:对图的所有边按照权值大小进行排序,然后按从小到大的顺序取出边。

                *在这里,通常定义一种存储边及其连接的顶点的结构体数组。

问题2  如何判断边及其顶点加入最小生成树是否会形成回路。

        应对策略:每条边机及其相连的顶点都视作一颗子树,然后判断这课子树的根是否和最小生成树的根相同;若相同,则会形成回路;若不同,则不会形成回路,将子树并入最小生成树。

1.4 适用场景(Kruskal 与 Prime 的比较)

1. Kruskal 算法 是将边作为操作对象,当加权图的边越多,要处理的边也越多,则算法的时间复杂度就越高;而顶点的数量对算法的时间复杂度无影响。所以,Kruskal 算法适合处理稀疏图(边较少的图)。时间复杂度:O(e\log e),e为边的个数。

2. Prime 算法 是将顶点作为操作对象,当加权图的顶点越多,则算法的时间复杂度就越高;而边的数量对算法的时间复杂度无影响。所以,Prime 算法适合处理稠密图(边较多的图)。时间复杂度:O(n^{2})

二、Kruskal 的实现

2.1 头文件

#include<stdio.h>

// 顶点的最大个数
#define MaxVertix 30
#define INF	32767		// INF infinite 无穷大,表权重无穷大

// 状态值
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

// 状态码 -- 状态值
typedef int Status;

// 1.邻接矩阵 - 存储结构
// 定点、边的类型
typedef char VertexType;   // 顶点的数据类型
typedef int ArcType;		// 边的数据类型

// 构造数据类型
typedef struct
{
	VertexType Verts[MaxVertix];
	ArcType UdArcs[MaxVertix][MaxVertix];			// 无向图 -- 矩阵表示法
	int VerNum;										// 顶点个数
	int ArcNum;										// 边的个数
}AMGraph;				// Adjacency  Matrix  Graph 邻接矩阵

// 狐结点类型 - 用狐表示的图结构
typedef struct
{
	int v1, v2;		// 狐关联的两顶点下标
	int Weight;		// 狐的权重
}Road;

// 函数声明
Status CreateUDN(AMGraph* G);	// 创建无向图

//最小生成树的 Kruskal 克鲁斯卡尔 算法
Status CreateGTree_Kruskal(AMGraph* G);	// 根据邻接矩阵图,从第 v 个结点构造 最小生成树

2.2 函数文件

#include "Graph.h"

// 获取 顶点 ver 的下标
int LocateVertex(AMGraph* G, VertexType* v)
{
	if (!G) return ERROR;

	int i;
	for (i = 0; i < G->VerNum; i++)
		if (*v == G->Verts[i])
			return i;

	return -1;	// 返回 -1 表示未找到顶点
}

// 创建无向图
Status CreateUDN(AMGraph* G)			// UndirectNet
{
	if (!G) return ERROR;

	printf("请输入顶点及边个数(Vers Arcs): ");
	scanf("%d %d", &G->VerNum,&G->ArcNum);
	getchar();

	int i;
	//录入顶点
	printf("\n请输入顶点的值(英文字母): ");
	for (i = 0; i < G->VerNum; i++)
	{
		do
		{
			VertexType v;
			scanf("%c", &v);		//scanf("%[a-zA-Z]", G->VerNum);	// 只接收26个英文字母
			getchar();
			if ((65 <= v && v <= 90) || (97 <= v && v <= 122))
			{
				G->Verts[i] = v;
				break;
			}
			printf("输入错误,请输入英文字母!\n");

		} while (1);	// do-while循环用于处理错误输入

	}
	
	//初始化所有边的权
	int j;
	for(i = 0; i < G->VerNum; i++)
		for (j = i; j < G->VerNum; j++)
		{
			G->UdArcs[i][j] = INF;	// 权重为无穷大,表示两顶点非邻接
			G->UdArcs[j][i] = INF;
		}

	//录入边的权值
	printf("\n请输入边关联的顶点及权值(v1 v2 weight): \n");
	for (i = 0; i < G->ArcNum; i++)
	{
		VertexType v1, v2;
		int w;
		do
		{
			scanf("%c %c %d", &v1, &v2, &w);
			getchar();

			if (v1 < 65 || (90 < v1 && v1 < 97) || 122 < v1)
			{
				printf("输入错误,请输入英文字母!\n");
				continue;
			}
			if (v2 < 65 || (90 < v2 && v2 < 97) || 122 < v2)
			{
				printf("输入错误,请输入英文字母!\n");
				continue;
			}
			//查找顶点位置
			int a, b;
			a = LocateVertex(G, &v1);
			b = LocateVertex(G, &v2);
		
			if (a < 0)		// 判断顶点是否存在
			{
				printf("输入的顶点%c不存在,请重新输入!\n",v1);
				continue;
			}
			if (b < 0)		// 判断顶点是否存在
			{
				printf("输入的顶点%c不存在,请重新输入!\n", v2);
				continue;
			}

			//链接到两顶点的边赋权值
			G->UdArcs[a][b] = w;
			G->UdArcs[b][a] = w;
			break;

		} while (1);	// do-while循环用于处理错误输入

	}
	return OK;
}

// 将狐按从小到大排序
Status Sort(Road R[], int e)
{
	if (!R) return ERROR;	// 处理空数组

	int i, j;
	// 冒泡排序
	for(i = 0; i < e - 1; i++)	// e 个数字,排 e - 1 趟。排完 1 趟 i+1
		for (j = 1; j < e - 1 - i; j++)	// 数字之间的大小关系所需要比较的次数。每排完一趟,比较次数-1
			if (R[j - 1].Weight > R[j].Weight)	// 前一个数大于后面一个数,交换两数的位置
			{
				Road tmp = R[j - 1];
				R[j - 1] = R[j];
				R[j] = tmp;
			}
	return OK;
}

 //获取结点所在子树的根
int GetRoot(int r[], int len, int v)		// len - 数组长度;  v - 结点下标
{
	if (!r) return -1;	// 处理空数组

	int i;
	for (i = 0; i < len; i++)
	{
		if (r[v] == v)	// 一个顶点存储的是自己的下标,则表示此顶点是一颗树的根结点
			return v;
		else v = r[v];
	}
	return -1;
}

// 根据邻接矩阵图,从第 v 个结点构造 最小生成树
Status CreateGTree_Kruskal(AMGraph* G)
{
/*	思路:以狐为单位,通过选取最小的狐来构造一颗颗局部的小树(小树也是最小生成树),
		  再将小树合并为一个大树,就得到完整的最小生成树(采用了局部最优得到整体最优的思想)
	步骤:
		1.选取最小的狐
		2.判断最小狐关联的两顶点是否属于同一颗树
		 a.若最小狐关联的两顶点属于不同的两颗树,就将这条狐和这两颗树合并到一颗树里面
		 b.若最小狐关联的两顶点属于同一颗树,舍弃这条狐(两顶点都在一颗树里面了,还并入这条狐,就出现回路了,就不是树结构了)
		3.重复上述步骤,直至得到一个完整的最小生成树		*/

	//处理空指针、非法下标
	if (!G)
		return ERROR; 

	Road road[MaxVertix];	// 记录狐关联的顶点。通过狐来表示出图中顶点与顶点、顶点与狐、狐与狐之间的关系

	// 记录每颗树的根结点下标,用于判断狐关联的两顶点是否属于同一棵树,防止回路
	// 数组下标对应顶点,数组存储的值是顶点所属树的根的下标
	int root[MaxVertix];

	int i,j,k = 0;
	for (i = 0; i < G->VerNum; i++)
	{
		root[i] = i;	//初始化:将每个顶点视作独立的一颗树

		// 寻找连通顶点的下标、狐的权重,并记录下来
		if(k < G->ArcNum)
			for (j = i+1; j < G->VerNum; j++)	// 无向图的邻接矩阵是对称的,所以只有统计上三角或者下三角即可
				if (G->UdArcs[i][j] < INF)	// 两顶点连通
				{
					road[k].v1 = i;		// 记住顶点1的小标
					road[k].v2 = j;		// 记住顶点2的小标
					road[k++].Weight = G->UdArcs[i][j];		// 记住两连通顶点关联的狐的权重
				}
	}

	Sort(road, G->ArcNum);	// 将狐按从小到大的顺序排序

	//寻找最小生成树
	for (i = 0; i < G->ArcNum; i++)	// 已是升序,每次都能去到最小的狐
	{
		int a = GetRoot(root,G->VerNum,road[i].v1);	// 获取结点所在树的根
		int b = GetRoot(root,G->VerNum,road[i].v2);

		if (a != b)		// 根结点不相同,则两结点不属于同一颗树。
		{
			root[a] = b;// 合并两棵树。将一颗树的根结点作为另一颗树的根(2棵树拥有同一个根时,就合二为一了)
			printf("%c--(%d)-->%c\n", G->Verts[road[i].v1], road[i].Weight, G->Verts[road[i].v2]);
		}
	}

	return OK;
}

GetRoot 函数的理解:

        整个算法执行的过程相当于是将多棵树拼装成一棵树,GetRoot 函数中的 for 循环相当于是寻找每棵子树的根结点,也就是说返回的 n 和 m 的值其实就相当于两棵子树的根节点,如果根结点相同,肯定就是同一棵树,即图连通了,出现了环路;如果不相等,就将两棵子树拼装成一棵树,即 root[a] = b。
        root 数组中下标 v 位置的值为 v,即 root[v] == v,表示找到根结点了。

root 数组的理解:

        root[a] = b,表示:将根结点为 b 的子树,并入根结点为 a 的子树,即两棵子树合并为一棵子树
        起初,root[i] = i ,表示:将每个顶点视作独立的一棵子树,即每个顶点都是一棵独立的树
        root[0] = 1,表示:下标为 0 的结点的前驱结点为 下标为 1 的结点

2.3 主函数(数据测试)

#include "Graph.h"    // 引用头文件 

int main()
{
	AMGraph amg;

	CreateUDN(&amg);

	int i, j;

	//打印邻接矩阵
	printf("\n图的邻接矩阵: \n  ");
	for (i = 0; i < amg.VerNum; i++)  // 打印列标
		printf("%-5c  ", amg.Verts[i]);

	for (i = 0; i < amg.VerNum; i++)
	{
		//printf("%c ", 'A' + i);
		for (j = 0; j < amg.VerNum; j++)
		{
			if (j > 0) printf(" ");
			else printf("\n%c ", amg.Verts[i]); // 打印行标
			printf("%-5d ", amg.UdArcs[i][j]); // 矩阵值
		}
	}

	//寻找最小生成树
	printf("\n\n最小生成树如下:\n");
	CreateGTree_Kruskal(&amg);

	return 0;
}

/* 测试数据 及 结果
	 __ A __
   1/   |   \
   /   2|    \3
  B—-7—-C—-6—-D
   \  4/    5/
  8 \ /     /
	 E—-9—-F

请输入顶点及边个数(Vers Arcs): 6 9

请输入顶点的值(英文字母): A B C D E F

请输入边关联的顶点及权值(v1 v2 weight):
A B 1
A C 2
A D 3
B C 7
B E 8
C D 6
C E 4
D F 5
E F 9

图的邻接矩阵:
  A      B      C      D      E      F
A 32767  1      2      3      32767  32767
B 1      32767  7      32767  8      32767
C 2      7      32767  6      4      32767
D 3      32767  6      32767  32767  5
E 32767  8      4      32767  32767  9
F 32767  32767  32767  5      9      32767

最小生成树如下:
A--(1)-->B
A--(2)-->C
A--(3)-->D
C--(4)-->E
D--(5)-->F	
            */

em ~ ~ ~ ~ ~ 

        在此,感谢您的倾心阅读!

        在茫茫人海中,你我能够相遇属实是一件难得的事!

        以上是本人的一些浅显理解,如有不妥之处,还望指出,咋们共同进步哟!

 

  • 15
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值