300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > 最小生成树(普里姆算法【Prim】与克鲁斯卡尔算法【Kruskal】)

最小生成树(普里姆算法【Prim】与克鲁斯卡尔算法【Kruskal】)

时间:2023-10-25 23:46:20

相关推荐

最小生成树(普里姆算法【Prim】与克鲁斯卡尔算法【Kruskal】)

写在前面:博主是一位普普通通的19届双非软工在读生,平时最大的爱好就是听听歌,逛逛B站。博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事,做自己以后不会留有遗憾的事,做自己觉得有意义的事,不浪费这大好的青春年华。博主写博客目的是记录所学到的知识并方便自己复习,在记录知识的同时获得部分浏览量,得到更多人的认可,满足小小的成就感,同时在写博客的途中结交更多志同道合的朋友,让自己在技术的路上并不孤单。

目录:

1.最小生成树

生成树概念

最小生成树

2.普里姆算法【Prim】

普里姆算法简介

普里姆算法完整代码实现 (C语言)

普里姆算法小结

3.克鲁斯卡尔算法【Kruskal】

克鲁斯卡尔算法简介

克鲁斯卡尔算法完整代码实现 (C语言)

克鲁斯卡尔算法小结

1.最小生成树

1.1生成树概念

连通图进行遍历,过程中所经过的边和顶点的组合可看做是一棵普通树,通常称为生成树生成树也理解为包含所有顶点的极小连通子图(极小连通子图后边会讲)

连通图中的生成树必须满足以下 2 个条件:

1.包含连通图中所有的顶点;

2.任意两顶点之间有且仅有一条通路;

因此,连通图的生成树具有这样的特征,即生成树中边的数量 = 顶点数 - 1

1.2最小生成树

对于含有 n 个顶点的连通图来说可能包含有多种生成树,例如图 1 所示:

上图中的连通图和它相对应的生成树,可以用于解决实际生活中的问题:假设 A、B、C 和 D 为 4 座城市,为了方便生产生 活,要为这 4 座城市建立通信。对于 4 个城市来讲,本着节约经费的原则,只需要建立 3 个通信线路即可,就如图 1(b) 中的任意一种方式。 在具体选择采用(b)中哪一种方式时,需要综合考虑城市之间间隔的距离,建设通信线路的难度等各种因素,将这些因素综合 起来用一个数值表示,当作这条线路的权值。

假设通过综合分析,城市之间的权值如图 2(a)所示,对于(b)的方案中,选择权值总和为 7 的两种方案最节约经费。 这就是本节要讨论的最小生成树的问题,简单得理解就是给定一个带有权值的连通图(连通网),如何从众多的生成树中筛选出 权值总和最小的生成树,即为该图的最小生成树

给定一个连通网,求最小生成树的方法有:普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法。

2.普里姆算法【Prim】

2.1普里姆算法简介

普里姆算法在找最小生成树时,将顶点分为两类:一类是在查找的过程中已经包含在树中的(假设为 A 类),剩下的是另一类 (假设为 B 类)

对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A

类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。 所走过的顶点和边就是该连通图的最小生成树。

例如,通过普里姆算法查找上图的最小生成树的步骤为: 假如从顶点 A 出发,顶点 B、C、D 到顶点 A 的权值分别为 2、4、2,所以,对于顶点 A 来说,顶点 B 和顶点 D 到 A 的 权值最小,假设先找到的顶点 B:

继续分析顶点 C 和 D,顶点 C 到 B 的权值为 3,到 A 的权值为 4;顶点 D 到 A 的权值为 2,到 B 的权值为无穷大(如 果之间没有直接通路,设定权值为无穷大)。所以顶点 D 到 A 的权值最小:

最后,只剩下顶点 C,到 A 的权值为 4,到 B 的权值和到 D 的权值一样大,为 3。所以该连通图有两个最小生成树:

2.2普里姆算法完整代码实现 (C语言)

#include <stdio.h>#include <stdlib.h>#define VertexType int#define VRType int#define MAX_VERtEX_NUM 20#define InfoType char #define INFINITY 65535typedef struct {VRType adj; //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info; //弧额外含有的信息指针}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];typedef struct {VertexType vexs[MAX_VERtEX_NUM]; //存储图中顶点数据AdjMatrix arcs; //二维数组,记录顶点之间的关系int vexnum,arcnum; //记录图的顶点数和弧(边)数}MGraph;//根据顶点本身数据,判断出顶点在二维数组中的位置int LocateVex(MGraph G,VertexType v){int i=0;//遍历一维数组,找到变量 vfor (; i<G.vexnum; i++) {if (G.vexs[i]==v) {return i;}}return -1;}//构造无向网void CreateUDN(MGraph* G){scanf("%d,%d",&(G->vexnum),&(G->arcnum));for (int i=0; i<G->vexnum; i++) {scanf("%d",&(G->vexs[i]));}for (int i=0; i<G->vexnum; i++) {for (int j=0; j<G->vexnum; j++) {G->arcs[i][j].adj=INFINITY;G->arcs[i][j].info=NULL;}}for (int i=0; i<G->arcnum; i++) {int v1,v2,w; scanf("%d,%d,%d",&v1,&v2,&w);int m=LocateVex(*G, v1);int n=LocateVex(*G, v2);if (m==-1 ||n==-1) {printf("no this vertex\n");return;}G->arcs[n][m].adj=w;G->arcs[m][n].adj=w;}}//辅助数组,用于每次筛选出权值最小的边的邻接点typedef struct {VertexType adjvex;//记录权值最小的边的起始点VRType lowcost;//记录该边的权值}closedge[MAX_VERtEX_NUM]; closedge theclose;//创建一个全局数组,因为每个函数中都会使用到//在辅助数组中找出权值最小的边的数组下标,就可以间接找到此边的终点顶点。int minimun(MGraph G,closedge close){int min=INFINITY;int min_i=-1;for (int i=0; i<G.vexnum; i++) {//权值为 0,说明顶点已经归入最小生成树中;然后每次和 min 变量进行比较,最后找出最小的。if (close[i].lowcost>0 && close[i].lowcost < min) {min=close[i].lowcost; min_i=i;}}//返回最小权值所在的数组下标return min_i;}//普里姆算法函数,G 为无向网,u 为在网中选择的任意顶点作为起始点void miniSpanTreePrim(MGraph G,VertexType u){//找到该起始点在顶点数组中的位置下标int k=LocateVex(G, u);//首先将与该起始点相关的所有边的信息:边的起始点和权值,存入辅助数组中相应的位置,例如(1,2)边,adjvex 为 0, lowcost 为 6,存入 theclose[1]中,辅助数组的下标表示该边的顶点 2for (int i=0; i<G.vexnum; i++) {if (i !=k) {theclose[i].adjvex=k;theclose[i].lowcost=G.arcs[k][i].adj;}}//由于起始点已经归为最小生成树,所以辅助数组对应位置的权值为 0,这样,遍历时就不会被选中theclose[k].lowcost=0;//选择下一个点,并更新辅助数组中的信息for (int i=1; i<G.vexnum; i++) {//找出权值最小的边所在数组下标k=minimun(G, theclose);//输出选择的路径printf("v%d v%d\n",G.vexs[theclose[k].adjvex],G.vexs[k]);//归入最小生成树的顶点的辅助数组中的权值设为 0theclose[k].lowcost=0;//信息辅助数组中存储的信息,由于此时树中新加入了一个顶点,需要判断,由此顶点出发,到达其它各顶点的权值是否比之前记录的权值还要小,如果还小,则更新for (int j=0; j<G.vexnum; j++) {if (G.arcs[k][j].adj<theclose[j].lowcost) {theclose[j].adjvex=k;theclose[j].lowcost=G.arcs[k][j].adj;}}}printf("\n");}int main(){MGraph G;CreateUDN(&G); miniSpanTreePrim(G, 1);}

用下图做例子:

2.3普里姆算法小结

普里姆算法的运行效率只与连通网中包含的顶点数相关,而和网所含的边数无关。所以普里姆算法适合于解决边稠密的网,该算法运行的时间复杂度为:O(n2)

如果连通网中所含边的绸密度不高,则建议使用克鲁斯卡尔算法求最小生成树(下面会讲)。

3.克鲁斯卡尔算法

3.1克鲁斯卡尔算法简介

对于任意一个连通网的最小生成树来说,在要求总的权值最小的情况下,最直接的想法就是将连通网中的所有边按照权值大小进行升序排序,从小到大依次选择。

由于最小生成树本身是一棵生成树,所以需要时刻满足以下两点:

生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路; 对于具有 n 个顶点的连通网,其生成树中只能有 n-1 条边,这 n-1

所以克鲁斯卡尔算法的具体思路是:将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。直到具有 n 个顶点的连通网筛选出来 n-1 条边为止。筛选出来的边和所有的顶点构成此连通网的最小生成树。

判断是否会产生回路的方法为:在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就处在一棵树中,如果继续连接就会产生回路;如果不一致,说明它们之间还没有任何关系,可以连接。假设遍历到一条由顶点 A 和 B 构成的边,而顶点 A 和顶点 B 标记不同,此时不仅需要将顶点 A 的标记更新为顶点 B 的标记,还需要更改所有和顶点 A 标记相同的顶点的标记,全部改为顶点 B 的标记。

如下例子,使用克鲁斯卡尔算法找最小生成树的过程为::

首先,在初始状态下,对各顶点赋予不同的标记(用颜色区别),如下图所示:

对所有边按照权值的大小进行排序,按照从小到大的顺序进行判断,首先是(1,3),由于顶点 1 和顶点 3 标记不同,所以 可以构成生成树的一部分,遍历所有顶点,将与顶点 3 标记相同的全部更改为顶点 1 的标记,如下:

其次是(4,6)边,两顶点标记不同,所以可以构成生成树的一部分,更新所有顶点的标记为:

其次是(2,5)边,两顶点标记不同,可以构成生成树的一部分,更新所有顶点的标记为:

然后最小的是(3,6)边,两者标记不同,可以连接,遍历所有顶点,将与顶点 6 标记相同的所有顶点的标记更改为顶点 1 的标记:

继续选择权值最小的边,此时会发现,权值为 5 的边有 3 个,其中(1,4)和(3,4)各自两顶点的标记一样,如果连接会产生回路,所以舍去,而(2,3)标记不一样,可以选择,将所有与顶点 2 标记相同的顶点的标记全部改为同顶点 3 相同的标记:

当选取的边的数量相比与顶点的数量小 1 时,说明最小生成树已经生成。所以最终采用克鲁斯卡尔算法得到的最小生成树为(6)所示

3.2克鲁斯卡尔算法完整代码实现 (C语言)

#include "stdio.h" #include "stdlib.h" #define MAX_VERtEX_NUM 20#define VertexType inttypedef struct edge{VertexType initial;VertexType end;VertexType weight;}edge[MAX_VERtEX_NUM];//定义辅助数组typedef struct {VertexType value;//顶点数据int sign;//每个顶点所属的集合}assist[MAX_VERtEX_NUM]; assist assists;//qsort 排序函数中使用,使 edges 结构体中的边按照权值大小升序排序int cmp(const void *a,const void*b){return ((struct edge*)a)->weight-((struct edge*)b)->weight;}//初始化连通网void CreateUDN(edge *edges,int *vexnum,int *arcnum){printf("输入连通网的边数:\n");scanf("%d %d",&(*vexnum),&(*arcnum));printf("输入连通网的顶点:\n");for (int i=0; i<(*vexnum); i++) {scanf("%d",&(assists[i].value)); assists[i].sign=i;}printf("输入各边的起始点和终点及权重:\n");for (int i=0 ; i<(*arcnum); i++) {scanf("%d,%d,%d",&(*edges)[i].initial,&(*edges)[i].end,&(*edges)[i].weight);}}//在 assists 数组中找到顶点 point 对应的位置下标int Locatevex(int vexnum,int point){for (int i=0; i<vexnum; i++) {if (assists[i].value==point) {return i;}}return -1;}int main(){int arcnum,vexnum; edge edges;CreateUDN(&edges,&vexnum,&arcnum);//对连通网中的所有边进行升序排序,结果仍保存在 edges 数组中qsort(edges, arcnum, sizeof(edges[0]), cmp);//创建一个空的结构体数组,用于存放最小生成树edge minTree;//设置一个用于记录最小生成树中边的数量的常量int num=0;//遍历所有的边for (int i=0; i<arcnum; i++) {//找到边的起始顶点和结束顶点在数组 assists 中的位置int initial=Locatevex(vexnum, edges[i].initial);int end=Locatevex(vexnum, edges[i].end);//如果顶点位置存在且顶点的标记不同,说明不在一个集合中,不会产生回路if (initial!=-1&& end!=-1&&assists[initial].sign!=assists[end].sign) {//记录该边,作为最小生成树的组成部分minTree[num]=edges[i];//计数+1 num++;//将新加入生成树的顶点标记全不更改为一样的for (int k=0; k<vexnum; k++) {if (assists[k].sign==assists[end].sign) {assists[k].sign=assists[initial].sign;}}//如果选择的边的数量和顶点数相差 1,证明最小生成树已经形成,退出循环if (num==vexnum-1) {break; }}}//输出语句for (int i=0; i<vexnum-1; i++) {printf("%d,%d\n",minTree[i].initial,minTree[i].end);}return 0;}

对于下图例子来说

输入和运行结果:

3.3克鲁斯卡尔算法小结

刚刚介绍了求最小生成树之普里姆算法。该算法从顶点的角度为出发点,时间复杂度为 O(n2),更适合与解决边的绸密度更高 的连通网。 本节所介绍的克鲁斯卡尔算法,从边的角度求网的最小生成树,时间复杂度为O(eloge)。和普里姆算法恰恰相反,更适合于求 边稀疏的网的最小生成树

本篇博客转载C语言中文网

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。