目录
一、何谓最小生成树?
认识最小生成树之前,先认识一下生成树。
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 = {},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={},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 ~ ~ ~ ~ ~
在此,感谢您的倾心阅读!
在茫茫人海中,你我能够相遇属实是一件难得的事!
以上是本人的一些浅显理解,如有不妥之处,还望指出,咋们共同进步哟!