最小生成树(普里姆算法【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 65535
typedef 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;
    //遍历一维数组,找到变量 v
    for (; 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]中,辅助数组的下标表示该边的顶点 2
   for (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]);
//归入最小生成树的顶点的辅助数组中的权值设为 0
   theclose[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 int
typedef 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)。和普里姆算法恰恰相反,更适合于求 边稀疏的网的最小生成树

猜你喜欢

转载自blog.csdn.net/qq_45737068/article/details/107932343