目录
一、最小生成树简介
用途:找到连通图的最短路径之和。
注:最小生成树能够保证整个拓扑图的所有路径之和最小,但不能保证任意两点之间是最短路径。
应用:要在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算法以及防止连通(防止连通是难点)
图不能相互连通,否则那就不叫“生成树”了。
首先begin和end是两个顶点,分别在边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;
}