最小生成树 Prim 算法

目录

一、何谓最小生成树?

1.0 生成树

1.1 最小生成树

二、如何求解最小生成树?Prim 算法

2.0 Prim 算法思想

2.1 举例

 三、代码实现

头文件 "Graph.h"

图相关函数

生成树相关函数(核心)

主函数文件(数据测试)


一、何谓最小生成树?

 认识最小生成树之前,先认识一下生成树。

1.0 生成树

        所谓的生成树,就是所有顶点均由边连接在一起,但不存在回路的图。

生成树的特点:

  1.生成树的顶点个树与图的顶点个数相同

  2.生成树是图的极小连通子图,去掉任意一条边都不在连通

  3.一个有 n 个顶点的连通图的生成树,有 n - 1 条边

  4.生成树中再加一条边必然形成回路

*一个图可以有许多棵不同的生成树

*含 n 个顶点 n-1 条的图不一定是生成树

示例:

1.1 最小生成树

        最小生成树:一个无向网的所有生成树中,各边权值之和最小的那颗生成树称为该无向网的最小生成树。

示例:

二、如何求解最小生成树?Prim 算法

        求最小生成树的方法很多,但是我们可以先从经典的方法入手。所谓:经典永不过时嘛!

2.0 Prim 算法思想

        Prim 算法在查找最小生成树的过程中,采用的谈心算法的思想。从局部最优到整体最优,每一步都是最优解,然后得到整体也是最优解。

Prim算法思路:

        1.将所有的顶点分为两类。假设 T类、G类,T 类 包含的是已经属于最小生成树的结点,G类包含的是非生成树的最小结点,T 与 G  互斥。

        2. 初始状态下,所有顶点都属于 G 类。

        3. 选择一个起点,移入 T 类。

        4. 从 T 类出发,选择一条到 G 类的最短的路径,然后将最短路径连接的G类顶点,移入T类集合中,同时记主这条最短路径。重复上述步骤,直到 G 类中的所有顶点都移入到 T 类集合中,恰好找到 N-1 条边,这样 T 类就是最小生成树了。

2.1 举例

以下面这张连通网为例,使用普里姆算查找最小生成树,需要经历以下几个过程:

 1.将图中的所有顶点分为 T 类 和 G 类,初始状态下,T = {\phi},G={A,B,C,D,E,F}。

 2.在 G 类中任选一个顶点作为 最小生成树的起点,假设选择顶点 A,然后将顶点 A 移到 G类中,就有 T={A},G={B,C,D,E,F}。

3.从T类的顶点A出发,与G类中的顶点相连通的边分别是权值为A->B=1,A->C=2,A->D=3 的三条边,选择权值为A->B=1那条最小的边作为最小生成树的一部分,将其所连的属于G类中的顶点B移到T类中,得到 T={A,B},G={C,D,E,F}。

4. 从T类的所有顶点出发(A,B),与G类中的顶点相连通的边分别是权值为A->C=2,A->D=3 ,B->C=7,B->E=8 的四条边,选择权值为A->C=2那条最小的边作为最小生成树的一部分,将其所连的属于G类中的顶点C移到T类中,得到 T={A,B,C},G={D,E,F}。

一直重复步骤3,4 直到 G={\o\phi},A={A,B,C,D,E,F},则A中包含的就是最小生成树的结点,在寻找最小生成树的结点中,记录已找到的树结点相连的边 border={A-B,A-C,A-D,C-E,D-F},共N-1条边(N为顶点数),即可得到最小生成树的完整信息了。

得到最小生成树

        

 三、代码实现

头文件 "Graph.h"

#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 Data[MaxVertix];
	int F;	// 头指针
	int R;  // 尾指针
	int L;	// 当前队列长度
}Queue;

//树结点类型  -- 采用兄弟结点表示法
typedef struct
{
	VertexType Data;	// 树结点的数据
	int Parent, FirstChild, Brother;	// 父结点、第一个孩子结点、兄弟结点在邻接矩阵图中的下标
	int Weight;	// 父结点到此结点的路径的权重	
}GTreeNode;

//树类型
typedef struct
{
	GTreeNode Nodes[MaxVertix];	// 采用数组存储结构
	int NodeNum;				// 树的结点个数
}MinGTree;	// Mininum Generate Tree 最小生成树

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

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

Status InitGTree(MinGTree* T,int n);	// 初始化一颗最小生成树。构造一颗有 n 个结点的树

//最小生成树的 Prim 普里姆 算法
Status CreateMinGTree_Prim(MinGTree* T, AMGraph* G, int v);	// 根据邻接矩阵图,从第 v 个结点构造 最小生成树

void TraverseGTree(MinGTree* T);	// 遍历一颗最小生成树

图相关函数

#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;
}

生成树相关函数(核心)

#include "Graph.h"

// 队列初始化 -- 形成一个空队
Status InitQueue(Queue* Q)
{
	if (!Q) return ERROR;	// 处理空指针
	
	int i;
	for(i = 0; i < Q->L; i++)
		Q->Data[i] = -1;

	Q->F = Q->R = 0;	// 空队列,首尾相接

	return OK;
}

// 入队
Status EnQueque(Queue* Q, unsigned int v)	// 数组的下标为非复数
{
	if (!Q) return ERROR;	// 处理空指针

	// 满队。数组表示的循序队列,
	// 为区分满队与空队,规定在入队方向上,
	// 队尾在队首相差一个位置时,为满队(而不是出队方向,出队方向说明队列还有一个未出队元素)
	if ((Q->R + 1) % Q->L == Q->F)	
		return ERROR;

	Q->Data[Q->R++] = v;	// 等价于 Q->Data[Q->R] = v; Q->R++; 队尾指针后移

	// 指针移到数组尾端,则跳转的首端,以实现循环队列结构。
	Q->R %= Q->L;
	// 此函数说的指针并非编程语言上的指针,而是方便标记队列位置的标记

	return OK;
}

// 元素出队
Status DeQueue(Queue* Q, int* value)
{
	if (!Q || !value) return ERROR; // 处理空指针

	if (Q->F == Q->R) return ERROR;	// 空队。无元素可出栈

	*value = Q->Data[Q->F++];	// 等价于 *value = Q->Data[Q->F]; Q->F++; 出队,队头指针后移

	// 指针移到数组尾端,则跳转的首端,以实现循环队列结构。
	Q->F %= Q->L;
	// 此函数说的指针并非编程语言上的指针,而是方便标记队列位置的标记

	return OK;
}

// 判断队列是否为空
Status EmptyQue(Queue* Q)
{
	if (!Q) return ERROR;	// 处理空指针

	if (Q->F == Q->R)	// 空队列,返回 TRUE == 1
		return TRUE;
	else 
		return FALSE;	// 非空队列,返回 FALSE == 0
}

// 初始化一颗生成树。构造一颗有n个结点的树
Status InitGTree(MinGTree* T, int n)
{
	if (!T) return ERROR;	// 处理空指针

	T->NodeNum = n;	// 0个树结点,表空树

	int i;
	for (i = 0; i < T->NodeNum; i++)
		T->Nodes[i].Parent = T->Nodes[i].FirstChild = T->Nodes[i].Brother = -1;

	return OK;
}

// 根据邻接矩阵图,从第 v 个结点构造 生成树
Status CreateMinGTree_Prim(MinGTree* T, AMGraph* G, int v)	// v 表示:将连通图的v结点作为树的根结点
{
/*	思路:
		1.以顶点为单位,划分树集合、非树集合
		2.寻找树集合到非树集合的最短路径
		3.将最短路径关联到的非树集合顶点转化为树集合的结点。树集合+1,非树集合-1
		4.重复上述步骤,直至非树集合为空集					*/

	//处理空指针、非法下标
	if (!T || !G || v < 0 || v > T->NodeNum) return ERROR;

	int lowCost[MaxVertix] = { 0 };	// 记录连通图中:树集合 到 非树集合权重最小的路径
	int vSet[MaxVertix] = { 0 };	// 用于区分连通图中的顶点是否已并入树中。0 未并入;1 并入
	int Family[2][MaxVertix] = { 0 };	// 第一行放父结点下标,第二行放孩子结点下标

	vSet[v] = 1;	// 根结点并入树中。
	T->Nodes[v].Data = G->Verts[v];

	int i;
	// 将根结点到非树结点的所有路径开销(权重)记录下来
	for (i = 0; i < G->VerNum; i++)
	{
		lowCost[i] = G->UdArcs[v][i];
		//记录路径所关联的顶点的父子关系
		if(lowCost[i] != INF)
		{
			Family[0][i] = v;
			Family[1][i] = i;
		}
	}

	int chl = 0;	// 记录孩子结点的下标

	//寻找树结点到非树结点的最小权重路径
	for (i = 0; i < G->VerNum - 1; i++)	// 根结点已并入树集合,非树集合 - 1
	{
		//min用于记录最小权重路径关联的非树集合中的结点
		int min = INF;	// 将初始最小权重设为无穷大(不是真正无穷大,而是表示大于所有路径)

		int j;
		//寻找权重最小的路径,并记录其下标
		for (j = 0; j < G->VerNum; j++)
		{
			if (vSet[j] == 0 && lowCost[j] < min)
				min = j;
		}
		if (min == INF) continue;

		vSet[min] = 1;	// 将找到的从树集合到非树集合的最小开销路径所关联的结点,并入到树集合中

		T->Nodes[min].Data = G->Verts[min];		// 赋入顶点数据
		T->Nodes[min].Weight = lowCost[min];	// 赋入路径权值
		int p = Family[0][min];		// 记录父结点下标
		T->Nodes[min].Parent = p;	// 孩子结点记录父结点下标

		//FirstChild 指向长子的下标; Brother 指向兄弟结点的下标
		if (T->Nodes[p].FirstChild == -1)
		{
			T->Nodes[p].FirstChild = Family[1][min];
			chl = T->Nodes[p].FirstChild;
		}
		else
		{
			T->Nodes[chl].Brother = Family[1][min];
			chl = T->Nodes[chl].Brother;
		}

		//更新 树集合 到 非树集合权重最小的路径
		for (j = 0; j < G->VerNum; j++)
		{
			// 开销最小路径 且 非连通到树结点。以保证是极小连通子图
			if (vSet[j] == 0 && G->UdArcs[min][j] < lowCost[j])
			{
				lowCost[j] = G->UdArcs[min][j];
				Family[0][j] = min;			// 更新了最短路径,则一并更新最短路径关联的顶点的父子关系
				Family[1][j] = j;
			}
		}
	}
	
	printf("\nTreeNodes: ");
	for (i = 0; i < T->NodeNum; i++)
		printf("%c ", T->Nodes[i].Data);

	printf("\nMinPath: ");
	for (i = 0; i < T->NodeNum; i++)
		printf("%d ", lowCost[i]);

	return OK;
}

// 遍历一个树
void TraverseGTree(MinGTree* T)
{
	/*
	逻辑梳理:
		因为此生成树是用兄弟表示法形成。
		所以,若要访问每层的结点,需要先找到每层的第一个孩子结点
		再由第一个孩子(长子)去访问其它的孩子(第一个孩子的所有兄弟)
	思路:
		1.建立队列,将生成树的每层的长子按层次顺序入队
		2.每层的长子出队,然后通过长子去访问其所有兄弟结点
	*/

	if (!T) return;	// 处理空指针

	Queue Q;
	Q.L = T->NodeNum;

	InitQueue(&Q);

	//EnQueque(&Q, 0);	// 根结点入队。根结点所在的第一层,只有自己,把自己视作此层的第一个孩子

	int i;
	//寻找每一层的长子,并让其入队
	for(i = 0; i < T->NodeNum - 1; i++)	// T->NodeNum - 1 表: 不查看最后一个结点是否有孩子(肯定没有)
		if (T->Nodes[i].FirstChild != -1)
			EnQueque(&Q, T->Nodes[i].FirstChild);

	while (!EmptyQue(&Q))	// 队列非空
	{
		int chl = -1,fah = 0;	// 依次代表: 孩子、父亲

		DeQueue(&Q, &chl);	// 孩子出队

		fah = T->Nodes[chl].Parent;	// 记住孩子父亲

		while (chl >= 0)	// 非负数,表示有孩子
		{
			printf("%c-%d->%c ", T->Nodes[fah].Data, T->Nodes[chl].Weight, T->Nodes[chl].Data);
			chl = T->Nodes[chl].Brother;
		}

		printf("\n"); // 一层遍历完毕,换行
	}
	return;
}

主函数文件(数据测试)

#include "Graph.h"			// 邻接矩阵存储结构

int main()
{
	/* 测试数据 及 结果
	 __ 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

TreeNodes: A B C D E F
MinPath: 32767 1 2 3 4 5

生成树:
A-1->B A-2->C A-3->D
C-4->E
D-5->F					*/

	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]);
		}
	}

	int loc = 0;
	VertexType v1 = 0;

	MinGTree tree;
	int n = amg.VerNum;
	InitGTree(&tree, n);

	printf("\n\n请输入将作为树根结点的图结点: ");
	scanf("%c", &v1);
	getchar();
	loc = LocateVertex(&amg, &v1);
	CreateMinGTree_Prim(&tree, &amg, loc);

	printf("\n\n生成树: \n");
	TraverseGTree(&tree);

	return 0;
}

em ~ ~ ~ ~ ~ 

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值