数据结构与算法(7-3)最小生成树(普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法)

目录

一、最小生成树简介

二、普里姆算法(Prim) 

1、原理  

2、存储

2-1、图顶点和权:

2-3、 最小生成树:

3、Prim()函数

3-1、新顶点入树

3-2、保留最小权

3-3、 找到最小路径

3-4、判断退出或递归

 4、代码

三、克鲁斯卡尔算法

1、原理

2、过程

 2-1、存储结构

2-2、从小到大排边

2-3、Kruskal算法以及防止连通(防止连通是难点)

3、代码

参考资料


一、最小生成树简介

用途:找到连通图的最短路径之和

注:最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径

应用:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。

最小生成树最短路径区别:

最小生成树连通图最短路径

最短路径:两任意结点之间(可以非邻接)的最短路径

  

二、普里姆算法(Prim) 

1、原理  

把树视为一个整体(顶点),从根部(自选定)出发,一点一点向周围搜索,找到周围权最小的顶点(最小路径),把它纳入Prim树,把Prim树的整体视作一个根结点,继续往下递归搜索

(欣赏下面的三样图)

1、

2、 

  3、

2、存储

2-1、图顶点和权:

//图(顶点和权)
typedef struct
{
	char vertex[MAXSIZE];
	int weight[MAXSIZE][MAXSIZE];			//权可以代替边(自身为0,相连有值,不相连无穷大)
}Graph;
Graph G;

不需要再加边,因为权=0自身权=无穷大断开,所以不需要再加边来表示连接关系。

2-3、 最小生成树:

//最小生成树
typedef struct
{
	char vertex[MAXSIZE];				//最小生成树内部顶点
	int weight[MAXSIZE];				//最小生成树权重(内部为0,相连有值,不相连无穷大)
	int minway[MAXSIZE];				//最小生成树最短路径
}MST;
MST M;

3、Prim()函数

3-1、新顶点入树

最小生成树也是越积越多,顶点vertex纳入最小生成树,则M.vertex[index]=vertex,其对应权M.weight[index]=0

M.vertex[]:存放最小生成树内部的顶点。

M.weight[]:存放最小生成树内部距离外界的最短路径。

3-2、保留最小权

新入树的顶点的权树本身的权进行比较保留更小的一方(因为要生成的是最短路径和)。

3-3、 找到最小路径

记录从最小生成树内部的顶点连向外界顶点最短路径最小权),保留下来即为本次行走的权。(也是下一次需要纳入的权)(先纳入顶点,在找下一次纳入的权)

3-4、判断退出或递归

如果Prim()函数调用次数达到顶点长度则退出递归,否则一直递归调用Prim()函数。

//获取最小生成树的最小权值下标
int FindMin()
{
	int i, min = 0;
	for (i = 0; i < length; i++)
	{
		if (M.weight[i] != 0)			//跳过0(即跳过内部顶点)
		{
			if (M.weight[min] == 0 || M.weight[min] > M.weight[i])	//跳过0,取最小
				min = i;
		}
	}
	return min;
}

//普里姆算法
void Prim(char vertex, int index)				//放入根
{
	int i, j, min;
	//获取最小生成树新的顶点
	M.vertex[index] = vertex;				//新顶点
	//获取最小生成树新的权
	M.weight[index] = 0;					//新权(纳入最小生成树内部,为0)
	for (i = 0; i < length; i++)
	{   
		if (M.weight[i] > G.weight[index][i])		//获得最小权
		{
			M.weight[i] = G.weight[index][i];		//最小生成树的权
		}

		//标记最小路径
		if (M.weight[i] == 0)
			for (j = 0; j < length; j++)
			{
				//												行!=列(0)		 i和j不能都在最小生成树内(不能连接自己)		
				if (M.minway[index]>G.weight[i][j] && j!=i && ((M.weight[i]!=0)||(M.weight[j]!=0)))	//i != j
					M.minway[index] = G.weight[i][j];
			}
	}

	printf("%c %d   ", M.vertex[index], M.minway[index]);

	count++;
	//判断退出
	if (count >= length)
		return;
	//寻找下一个最小生成树下标(跳过0)
	min = FindMin();
	Prim(G.vertex[min], min);
}

 4、代码

//普里姆算法(Prim)————图的最小生成树
//把树视为一个整体(顶点),从根部(自选定)出发,一点一点向周围搜索,
//找到周围权最小的顶点(最小路径),把它纳入Prim树,把Prim树的整体视作一个根结点,
//继续往下递归搜索。
//自实现,目前有一定的缺点:时间复杂度O(n^3)有些高
/*测试:
ABCDEFGHI
B 10 F 11
C 18 I 12 G 16
B 18 I 8 D 22
C 22 I 21 G 24 H 16 E 20
D 20 H 7 F 26
A 11 G 17 E 26
B 16 D 24 F 17 H 19
D 16 E 7 G 19
B 12 C 8 D 21
*/
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

#define MAXSIZE 20
#define MAX 65535					//代表无穷大
int length = 0;							//顶点个数
int count = 0;							//计数Prim最小生成树元素个数

//图(顶点和权)
typedef struct
{
	char vertex[MAXSIZE];
	int weight[MAXSIZE][MAXSIZE];			//权可以代替边(自身为0,相连有值,不相连无穷大)
}Graph;
Graph G;

//最小生成树
typedef struct
{
	char vertex[MAXSIZE];				//最小生成树内部顶点
	int weight[MAXSIZE];				//最小生成树权重(内部为0,相连有值,不相连无穷大)
	int minway[MAXSIZE];				//最小生成树最短路径
}MST;
MST M;

//输入顶点
void InputVertex()
{
	int i;
	char ch;
	printf("请输入图的顶点:\n");
	scanf("%c", &ch);
	for (i = 0; i < MAXSIZE && ch != '\n'; i++)
	{
		G.vertex[i] = ch;
		scanf("%c", &ch);
	}
	length = i;
}

//图权重初始化
void GraphWeightInit()
{
	int i, j;
	for (i = 0; i < length; i++)
	{
		for (j = 0; j < length; j++)
		{
			if (i == j)							//指向自己
				G.weight[i][j] = 0;
			else
				G.weight[i][j] = MAX;	//无穷大
		}
	}
}

//根据数据查找图顶点下标
int FindIndex(char ch)
{
	int i;
	for (i = 0; i < length; i++)
	{
		if (G.vertex[i] == ch)
			return i;
	}
	return -1;
}

//获取最小生成树的最小权值下标
int GetMin()
{
	int i, min = 0;
	for (i = 0; i < length; i++)
	{
		if (M.weight[i] != 0)			//跳过0(即跳过内部顶点)
		{
			if (M.weight[min] == 0 || M.weight[min] > M.weight[i])	//跳过0,取最小
				min = i;
		}
	}
	return min;
}

//创建图
void CreateGraph()
{
	int i, j, index, weight;
	char ch;
	for (i = 0; i < length; i++)
	{
		printf("请输入%c的邻接顶点及权重(空格分隔,换行结束):\n", G.vertex[i]);
		scanf("%c", &ch);
		while (ch != '\n')
		{
			while (ch == ' ')				//为空格
			{
				scanf("%c", &ch);			//输入字符
				continue;
			}
			index = FindIndex(ch);
			scanf("%d", &weight);		//输入权重
			while (weight == 32)		//32为空格的ASCII码
			{
				scanf("%d", &weight);
				continue;
			}
			G.weight[i][index] = weight;	//存入权重
			scanf("%c", &ch);				//(下一轮)输入字符
		}
	}
}

//最小生成树初始化
void MST_Init()
{
	for (int i = 0; i < length; i++)
	{
		M.weight[i] = MAX;							//权初始设置为无穷大(无邻接结点)
		M.minway[i] = MAX;						//最短路径值
	}
}

//普里姆算法
void Prim(char vertex, int index)				//放入根
{
	int i, j, min;
	//获取最小生成树新的顶点
	M.vertex[index] = vertex;				//新顶点
	//获取最小生成树新的权
	M.weight[index] = 0;					//新权(纳入最小生成树内部,为0)
	for (i = 0; i < length; i++)
	{
		if (M.weight[i] > G.weight[index][i])		//刷新最小权
		{
			M.weight[i] = G.weight[index][i];			//覆盖最小生成树的权
		}

		//找到最小路径(最小生成树)
		if (M.weight[i] == 0)					//最小生成树内部
			for (j = 0; j < length; j++)
			{
				//												  行!=列(0)		 i和j不能都在最小生成树内(不能连接自己)(i在内, j在外)		
				if (M.minway[index] > G.weight[i][j] && j != i && ((M.weight[i] != 0) || (M.weight[j] != 0)))	//i != j
					M.minway[index] = G.weight[i][j];
			}
	}

	printf("%c -> %d  -> ", M.vertex[index], M.minway[index]);

	count++;
	//判断退出
	if (count >= length)
		return;
	//寻找下一个最小生成树下标(跳过0)
	min = GetMin();
	Prim(G.vertex[min], min);						//递归Prim()函数
}

//输出测试
void Print()
{
	for (int i = 0; i < length; i++)
	{
		printf("\n%c结点邻接结点:\t", G.vertex[i]);
		for (int j = 0; j < length; j++)
		{
			if (G.weight[i][j] != 0 && G.weight[i][j] != MAX)		//有邻接结点
			{
				printf("%c %d\t", G.vertex[j], G.weight[i][j]);
			}
		}
	}
}

int main()
{
	InputVertex();				//输入顶点

	GraphWeightInit();		//图权重初始化

	CreateGraph();				//创建图

	MST_Init();					//最小生成树初始化

	printf("\n最小生成树路径及权(Prim算法):\n");
	Prim(G.vertex[0], 0);		//普里姆算法(最小生成树)

	//Print();						//测试输出

	return 0;
}

(这个算法是自实现的,时间复杂度有些高,O(n^3),先暂时不去优化,继续往后学吧)

三、克鲁斯卡尔算法

1、原理

为基础,从小到大依次选出最短路径,产生最小生成树。

原理图:

第1步:将边<E,F>加入R中。

边<E,F>的权值最小,因此将它加入到最小生成树结果R中。

第2步:将边<C,D>加入R中。

上一步操作之后,边<C,D>的权值最小,因此将它加入到最小生成树结果R中。

第3步:将边<D,E>加入R中。

上一 步操作之后,边<D,E>的权值最小,因此将它加入到最小生成树结果R中。

第4步:将边<B,F>加入R中。

上一步操作之后,边<C,E>的权值最小,但<C,E>会和已有的边构成回路;因此,跳过边<C,E>。同理,跳过边<C,F>.将边<B,F>加入到最小生成树结果R中。

第5步:将边<E,G>加入R中。

上一步操作之后,边<E,G>的权值最小,因此将它加入到最小生成树结果R中。

第6步:将边<A,B>加入R中。

上一步操作之后,边<F,G>的权值最小,但<F,G>会和已有的边构成回路;因此,跳过边<F,G>。同理,跳过边<B,C>.将边<A,B>加入到最小生成树结果R中。

此时,最小生成树构造完成!它包括的边依次是: <E,F> <C,D> <D,E><B,F>  <E,G> <A,B>.

2、过程

 2-1、存储结构

1、图的顶点

//图的顶点
char vertex[MAXSIZE];

2、图的边、克鲁斯卡尔(最小生成树数组)

//图的边(以边为主体,装入两端顶点及权)
typedef struct Edge
{
	int begin;
	int end;
	int weight;
}Edge;
Edge E[MAXSIZE];			//边数组
Edge K[MAXSIZE];			//Kruskal数组

2-2、从小到大排边

克鲁斯卡尔算法按照边的大小顺序,从小到大排列,需要有序的边

2-3、Kruskal算法以及防止连通(防止连通是难点)

图不能相互连通,否则那就不叫“生成树”了。

首先beginend是两个顶点,分别在边weight的左右。

防止连通原理:一个树上的任何结点,都可以追溯到相连通的尾部,如果追溯到尾部元素和新添加的元素一样,那么则会产生连通,此时这两个结点不能连接。

这里设置了一个circle[]数组,为了防止连通。

追溯尾部元素的代码:

//根据顶点查找到尾(下标追溯)
int FindTail(char ch)
{
	int index = FindIndex(ch);
	while (circle[index] != -1)
	{
		index = circle[index];	//追溯到尾(下标追溯)
	}
	return index;				//返回尾(没有连接顶点的话返回自身)
}

Kruskal算法代码:

//克鲁斯卡尔(Kruskal)算法
//难点:是否连通判断:需要追溯到尾,如果连通的话它们有共同的尾
void Kruskal()
{
	int i, now = 0, tail = 0;					//检测连通(头和尾)

	for (i = 0; i < length_e; i++)			//遍历每条边
	{
		tail = FindTail(E[i].begin);			//获取下标并追溯到尾(无连通则返回自身)
		now = FindTail(E[i].end);			//获取下标并追溯到尾
		//未连通,正常添加
		if (tail != now)
		{
			circle[tail] = now;							//尾连通(标识连通)
			K[count_k].begin = E[i].begin;		//左顶点
			K[count_k].end = E[i].end;			//右顶点
			K[count_k].weight = E[i].weight;	//中间边权
			printf("%c -- %d --%c\t", K[count_k].begin, K[count_k].weight, K[count_k].end);
			count_k++;
		}
	}
}

3、代码

//克鲁斯卡尔(Kruskal)算法
//测试案例:
/*ABCDEFG
12
12 AB
14 AG
16 AF
7 BF
9 FG
10 BC
8 EG
6 CF
2 EF
5 CE
3 CD
4 DE*/

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

#define MAXSIZE 20
#define MAX 65535
int length_v = 0;						//顶点个数
int length_e = 0;						//边个数
int circle[MAXSIZE] = { -1 };		//判断是否连通(里面的元素定位vertex[ ]中的元素)
int count_k;								//计数克鲁斯卡尔顶点

//图的顶点
char vertex[MAXSIZE];

//图的边(以边为主体,装入两端顶点及权)
typedef struct Edge
{
	int begin;
	int end;
	int weight;
}Edge;
Edge E[MAXSIZE];			//边数组
Edge K[MAXSIZE];			//Kruskal数组


//输入顶点
void InputVertex()
{
	int i;
	char ch;
	printf("请输入图的全部顶点(换行结束):\n");
	scanf("%c", &ch);
	for (i = 0; i < MAXSIZE && ch != '\n'; i++)
	{
		vertex[i] = ch;
		scanf("%c", &ch);
	}
	length_v = i;
}

//创建图
void CreateGraph()
{
	int weight = 0;
	char ch;
	printf("请输入边的数量:\n");
	scanf("%d", &length_e);
	printf("请分别输入边的权重和左右顶点:\n");
	for (int i = 0; i < length_e; i++)		//每一条边
	{
		printf("第%d条边:\t", i + 1);
		scanf("%d", &weight);					//权重
		while (weight == 32 && weight == 13)		//空格判断(32为空格的ASCII码,13为回车的ASCII码)
			scanf("%d", &weight);
		E[i].weight = weight;

		scanf("%c", &ch);				//第一个顶点
		while (ch == ' ')
			scanf("%c", &ch);
		E[i].begin = ch;

		scanf("%c", &ch);				//第一个顶点
		while (ch == ' ')
			scanf("%c", &ch);
		E[i].end = ch;
	}
}

//排序(按照边的权重,从低到高)
void Sort()
{
	int i, j, min;
	Edge temp;
	for (i = 0; i < length_e; i++)
	{
		min = i;
		for (j = i; j < length_e; j++)
		{
			if (E[min].weight > E[j].weight)
				min = j;
		}
		if (min != i)
		{
			temp = E[min];
			E[min] = E[i];
			E[i] = temp;
		}
	}
}

//根据顶点查找下标
int FindIndex(char ch)
{
	for (int i = 0; i < length_v; i++)
	{
		if (ch == vertex[i])
			return i;
	}
	return -1;
}

//根据顶点查找到尾(下标追溯)
int FindTail(char ch)
{
	int index = FindIndex(ch);
	while (circle[index] != -1)
	{
		index = circle[index];	//追溯到尾(下标追溯)
	}
	return index;					//返回尾(没有连接顶点的话返回自身)
}

//循环数组初始化
void Circle_Init()
{
	for (int i = 0; i < length_v; i++)
	{
		circle[i] = -1;
	}
}

//克鲁斯卡尔(Kruskal)算法
//难点:是否连通判断:需要追溯到尾,如果连通的话它们有共同的尾
void Kruskal()
{
	int i, now = 0, tail = 0;					//检测连通(头和尾)

	for (i = 0; i < length_e; i++)			//遍历每条边
	{
		tail = FindTail(E[i].begin);			//获取下标并追溯到尾(无连通则返回自身)
		now = FindTail(E[i].end);			//获取下标并追溯到尾
		//未连通,正常添加
		if (tail != now)
		{
			circle[tail] = now;							//尾连通(标识连通)
			K[count_k].begin = E[i].begin;		//左顶点
			K[count_k].end = E[i].end;			//右顶点
			K[count_k].weight = E[i].weight;	//中间边权
			printf("%c -- %d --%c\t", K[count_k].begin, K[count_k].weight, K[count_k].end);
			count_k++;
		}
	}
}

//逐边遍历(测试输出)
void Traverse_Edge()
{
	int i;
	for (i = 0; i < length_e; i++)
	{
		printf("\n第%d条边:\t 权重:%d\t 顶点1:%c\t 顶点2:%c\t ", i + 1, E[i].weight, E[i].begin, E[i].end);
	}
}

int main()
{
	InputVertex();			//输入顶点

	CreateGraph();			//创建图

	Sort();						//排序

	Circle_Init();				//循环数组初始化
	printf("克鲁斯卡尔算法计算最小生成树:\n");
	Kruskal();					//克鲁斯卡尔(Kruskal)算法

	//Traverse_Edge();		//逐边遍历(测试输出)

	return 0;
}

参考资料

https://blog.csdn.net/guozhangjie1992/article/details/106821932?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162833230916780261971711%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=162833230916780261971711&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-2-106821932.pc_search_result_control_group&utm_term=%E5%85%8B%E9%B2%81%E6%96%AF%E5%8D%A1%E5%B0%94%E8%BF%9E%E9%80%9A%E5%88%A4%E6%96%AD&spm=1018.2226.3001.4187 

https://www.bilibili.com/video/BV1jW411K7yg?p=64

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_(*^▽^*)_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值