最小生成树

​ 本部分主要对图中一些关于连通性和最小生成树的概念进行学习和理解,之后对于建立最小生成树的两种算法进行学习和实现。

​ 接下来主要对于无向图进行分析,有向图也是一样的分析方法。

文章目录

  • 最小生成树
    • 图的连通性以及生成树
    • 图的连通性判断
    • 最小生成树
    • 普里姆算法PrimPrimPrim
    • 克鲁斯卡尔算法KruskalKruskalKruskal

图的连通性以及生成树

​ 首先对图连通性中一些定义进行阐释:

  1. 在无向图中,如果存在不连通的顶点,则该图称为非连通图。
  2. 非连通图的最大连通子图叫做连通分量。
  3. 若从无向图的每一个连通子图中的一个顶点出发进行DFSDFSDFS或BFSBFSBFS遍历,可求得无向图的所有连通分量的生成树(DFSDFSDFS或BFSBFSBFS生成树)。
  4. 所有连通分量的生成树组成了非连通图的生成森林。

​ 在上述定义中,其实连通图也就代表着图中任意两个顶点之间都至少有一条路径可以彼此到达。

​ 对于一个非连通图来说,可以将其划分为多个连通图,也被称为原来的非连通图的连通子图。其中最大(包含顶点最多)的连通子图叫做连通分量。那么对于一个连通图而言,连通分量就是它自己。下图即为记忆额非连通图。

​ 对于DFSDFSDFS或BFSBFSBFS生成树,其实就是根据图的DFSDFSDFS遍历顺序和DFSDFSDFS遍历顺序来建立一棵树。这里注意的是,在使用BFSBFSBFS或DFSDFSDFS生成树的时候,一个连通子图的遍历结果是一棵树,多个连通子图所构成的森林组成了生成森林。接下来通过两个例子来进行进一步的了解。

图的连通性判断

​ 在对图的连通性有了一定的了解和学习之后,那么在给定一个图的时候,如何判断该图是否是一个连通图呢。这里其实方法有很多。接下来简单给出几个方法。

  1. 从某个顶点开始DFSDFSDFS或BFSBFSBFS遍历,遍历时统计遍历到的顶点数量,如果遍历到了所有的顶点,则说明是连通图,反之则是非连通图。
  2. 通过计算出任意两点之间的距离来进行判断,如果距离不是无穷大(即可达),则说明是连通图,反之则是非连通图。
  3. 使用并查集,也就是在建立生成树之后,查看所有顶点的根节点是否相同,如果相同则代表是连通图,反之则是非连通图。

总而言之,判断方法各式各样,接下来主要是通过DFSDFSDFS遍历来判断一个图是否是连通图。

​ 在使用DFSDFSDFS判断一个图是否是连通图时,只需要统计中间遍历了多少个点即可,这一步在使用代码实现时其实可以通过判断访问数组visitvisitvisit中有多少个被置为了truetruetrue即可。

   // 判断是否是连通图void judge(){// 初始化访问数组for (int i = 0; i < vexnum; i++)visit[i] = false;// 使用dfs来进行判断dfsMethod(0);int count = 0;  // 被访问的点的个数// 判断是否所有的点都被访问到for (int i = 0; i < vexnum; i++){if (visit[i])count++;}if (count == vexnum)cout << "连通图" << endl;elsecout << "非连通图" << endl;}// DFSvoid dfsMethod(int index){cout << index << endl;  // 输出当前遍历的结点信息visit[index] = true;    // 修改访问状态,代表已经访问过for (int i = 0; i < vexnum; i++)   // 将当前结点相连的结点逐个进行遍历{if (visit[i] == false && map[index][i] != 0)dfsMethod(i);}}

最小生成树

​ 在对连通性和连通图有了一定的了解之后,接下来对于最小生成树进行学习和了解。

​ 在了解最小生成树之前,首先对于一些定义进行一些阐释:

  1. 如果无向图中,边上有权值,则称该无向图为无向网。
  2. 如果无向网中每个顶点都相通,则该网被称为连通网
  3. 最小生成树(Minimum Cost Spanning Tree)是代价最小的连通网的生成树,即该生成树上的边的权值和最小

根据这些定义,可以知道最小生成树是基于一个连通图的,其实也可以理解为简化一个连通图,即删去一些边的同时保持其连通性,并使得剩下的边上的权值之和最小。

​ 根据上述分析可以得到建立最小生成树的一些准则:

  1. 必须使用且仅使用连通网中的n−1n-1n−1条边来联结网络中的nnn个顶点。
  2. 不能使用产生回路的边
  3. 各边上的权值的综合达到最小

最小生成树一般用于道路建设等,举一个具体例子来说,有nnn个村庄,现在需要在nnn个村庄之间建立公路,如何建立公路使得每个村庄互相到达且为了减少成本,需要使得总的公路长度最小。对于这个问题,其实就是需要建立最小生成树。

​ 在建立最小生成树时,有两种较为常用的方法,即PrimPrimPrim和KruskalKruskalKruskal算法,接下来对于这两种算法进行介绍。

普里姆算法PrimPrimPrim

​ PrimPrimPrim算法是建立最小生成树的算法之一,首先列出该算法的所有流程,之后对于该流程再进一步进行具体分析。假设N=(V,E)N=(V,E)N=(V,E)是连通网。TETETE是NNN上最小生成树中边的集合,u0u_0u0​是起始点,也就是最开始建立生成树的位置。

  1. U=u0U={u_0}U=u0​,u0∈Vu_0 \in Vu0​∈V,TE={}TE=\left\{ \right\}TE={}。
  2. 在所有的u∈Uu \in Uu∈U,v∈E′v \in E'v∈E′中找到一条权重最小的边(u,v0)(u,v_0)(u,v0​)并加入集合TETETE,同时v0v_0v0​并入UUU。其中E′E'E′中是由V−UV-UV−U中顶点所构成的子图中的边vvv,v∈Ev \in Ev∈E。
  3. 重复2步骤,直到U=VU=VU=V,则所得的T=(V,TE)T=(V,TE)T=(V,TE)即为所求最小生成树。

上述步骤其实仔细看来也较为简单,简单来说,PrimPrimPrim算法的总的思路为,初始给一个点,也就是起点,然后每一轮根据一个固定的算法来选择一个点添加到生成树点集中,当生成树点集中的点与原图中的点相同时,该树就完成了建立。

​ 那么核心就在于每一轮如何进行选点。首先可以确定一点的是,每次添加的点都是不存在于当前生成树点集中的点,即新添加的点vo∉Uv_o \notin Uvo​∈/​U。那么根据上述算法,所要找的点要满足一个特点,即u∈Uu \in Uu∈U,v∈E′v \in E'v∈E′中权重最小的一条边。这一点包含两条信息,首先是所要添加的点应该是不存在于UUU中但是于UUU中点邻接的点;其次是所要找的点与UUU中的点所构成的边的权重,要是当前这种边集E’E’E’中权重最小的,如果一个点被多个点邻接,取距离最短的一个。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GjLJ3eEc-1629042852874)(…\6.图\pics\895cd8638476ce9b8f49c97c10e1c23.png)]

​ 故通过上面的分析,PrimPrimPrim算法其实就是逐点添加的算法,接下来通过一个例子进行实际运算。

​ 在上图中,首先起点是0点,生成树点集中也就只有0,那么根据原图可知,0的邻接点为1和5,权重分别是28和10,那么根据权重大小,选择最小的,故选择5结点,之后将5号结点也纳入生成树点集中。第二轮时,生成树点集的邻接点为1和4,根据权重大小关系选择4结点(25<28),并将4结点添加到生成树点集中。之后的过程以此类推。

​ 上述即为PrimPrimPrim算法的基本过程,那么接下来就是到具体部分,即代码实现部分。在实现的时候,首先需要一个访问数组,因为不能重复添加某个点,之后需要一个用于记录生成树点集到邻接点的距离的数组,用一个数组disdisdis来表示,dis[i]dis[i]dis[i]代表生成树点集到第iii个结点的距离,配合visitvisitvisit数组进行使用即可。

​ 在实现时首先根据起始点初始化visitvisitvisit数组并结合邻接矩阵来初始化disdisdis数组,之后通过循环来逐个添加点。在内部循环中,首先需要做的是找出距离最小的新点,并将其添加到点集中(visit[i]=true)(visit[i]=true)(visit[i]=true)。之后需要更新距离,例如原本从2结点到5结点距离是99,此时往点集中添加了3结点,但是3结点也与5结点邻接并且距离只有10,那么就将这个距离从99更新为10。代码实现如下所示。

// Prim算法void Prim(int start){bool visit[MAX_NUM];    // 访问数组,用于判断该节点是否被访问过// 初始化全部为false,即没有被访问过for (int i = 0; i < MAX_NUM; i++)visit[i] = false;visit[start] = true;    // start为起始结点,已经被访问int dis[MAX_NUM];   // 到各个结点目前最近的距离int pre[MAX_NUM];   // 前驱,即距离各个结点最近的结点的下标for (int i = 0; i < vexnum; i++){dis[i] = map[start][i]; // 设置距离if (map[start][i] == INF || i == start) // 不可达则设置前驱为-1pre[i] = -1;else    // 可达则设置前驱pre[i] = start;}string ans_begin[MAX_NUM];string ans_end[MAX_NUM];int ans_weight[MAX_NUM];int all_path = 0;for (int i = 0; i < MAX_NUM; i++){ans_begin[i] = "";ans_end[i] = "";ans_weight[i] = -1;}// 逐点添加,已经添加了start,则继续添加vexnum - 1 个点即可for (int i = 0; i < vexnum - 1; i++){int min_dis = INF;  // 当前轮的最小距离int min_index = -1; // 所要添加的结点的下标for (int j = 0; j < vexnum; j++)    // 暴力遍历即可{// 从未被访问的点中找出一个距离最小的if (visit[j] == false && min_dis > dis[j])  {min_dis = dis[j];min_index = j;}}if (min_index == -1)    // 没有最小的continue;visit[min_index] = true;    // 设置标记为访问过if (pre[min_index] == -1)   // 错误情况break;// cout << pre[min_index] << " " << min_index << " " << dis[min_index] << endl;cout << nodes[pre[min_index]] << " " << nodes[min_index] << " " << dis[min_index] << endl;all_path += dis[min_index];// 更新距离向量for (int j = 0; j < vexnum; j++){if (visit[j] == false && map[min_index][j] < dis[j]){dis[j] = map[min_index][j];pre[j] = min_index;}}}cout << all_path << endl;}

克鲁斯卡尔算法KruskalKruskalKruskal

​ KruskalKruskalKruskal算法是另一种建立最小生成树的算法,接下来也是先介绍该算法的整体过程,之后进行更具体的分析。首先假设N=(V,E)N=(V,E)N=(V,E)是连通网。

  1. 非连通图T={V,{}}T=\left\{V,\left\{ \right\} \right\}T={V,{}},图中每个顶点自成一个连通分量。
  2. 在EEE中找一个权重最小,且其两个顶点分别依附在不同的连通分量的边,将其加入TTT中。
  3. 重复二,直到TTT中所有顶点都在同一个连通分量。

从上述过程中可以看出,KruskalKruskalKruskal算法的思路在于,首先将原图中所有的边去除,这样每个点单独属于一个连通分量,之后通过一个固定算法往图中添加边,直到该图变成一个连通图位置。

​ 在进行选边的时候,根据流程中的规定,主要遵循两点要求,首先是添加进去的边的两端点不能处在同一个连通分量,其次是权重最小。每次找到边权重最小的符合条件的边进行添加即可,这样一来每次都可以减少一个连通分量,直到最后只剩一个连通分量。

​ 那么根据上述分析,KruskalKruskalKruskal算法其实是一个逐边添加的算法,这与PrimPrimPrim算法是不同的。

​ 接下来通过一个例子来对该算法进行一个简单的学习。

​ 如上图所示,一开始去掉所有的边,之后由于10是最短的边,且0和5不在同一个连通分量,故添加0-5边。之后10虽然权重最短,但是0和5已经在同一个连通分量,故此时只能退而求其次,选择权重为12的2-3边,因为2和3此时属于不同的连通分量。之后的过程以此类推。

​ 上述即为KruskalKruskalKruskal算法的具体过程,接下来主要对于代码实现部分进行具体分析。

​ 在进行代码实现的时候,由于每次添加的都是一条边,故这里需要一个数据结构来表示边的内容,即起点,终点以及权重。边的定义如下所示。

// 边
typedef struct Edge
{string begin;   // 弧尾string end;     // 弧头int weight;     // 权重,距离// 构造函数Edge(){begin = "";end = "";weight = -1;}
}Edge;

​ 之后由于需要逐个添加,且总边集始终不会变化,故这里直接对整体先进行一次排序即可,按照升序,这样按照数组顺序进行遍历并结合另一条规定即可。排序代码如下所示。

// 对边集进行排序void sortEdge(){for (int i = 0; i < arcnum - 1; i++){for (int j = 0; j < arcnum - i - 1; j++){if (edge[j].weight > edge[j + 1].weight){Edge temp = edge[j];edge[j] = edge[j + 1];edge[j + 1] = temp;}}}for (int i = 0; i < arcnum; i++){cout << edge[i].begin << " " << edge[i].end << " " << edge[i].weight << endl;}cout << "***********" << endl;}

​ 那么接下来重点在于如何判断两个结点是否处于同一个连通分量了。这时候就需要使用到并查集的知识了,其实并查集也就是定义一个数组,将一个树中所有结点的该数组值设置为该根。这样一来就可以利用该数组中的值是否相同来判断两个节点是否处于同一个连通分量(加边的过程其实就是每个连通分量建立生成树的过程)。在添加完成之后记得更新一次并查集即可。

// 更新根void updateRoot(int root[], int old_root, int new_root){for (int i = 0; i < vexnum; i++)    // 遍历所有结点{if (root[i] == old_root)    // 换根root[i] = new_root;}}// Kruskal算法void Kruskal(){sortEdge(); // 先排序int root[MAX_NUM];   // 根数组for (int i = 0; i < MAX_NUM; i++)    // 初始化根就是自己root[i] = i;int all_path = 0;   // 总长度int count = 0;  // 记录已经添加的边的数量,最多只需要vexnum-1条即可for (int i = 0; i < arcnum; i++)    // 遍历所有的边,直到添加了vexnum-1条边为止{int index1 = getIndex(edge[i].begin);   // 获取下标int index2 = getIndex(edge[i].end);if (index1 == -1 || index2 == -1)   // 异常break;if (root[index1] == root[index2])   // 根相同说明添加改变不会增加新的结点continue;cout << edge[i].begin << " " << edge[i].end << " " << edge[i].weight << endl;all_path += edge[i].weight;updateRoot(root, index1, index2);   // 更新根节点count++;    // 已添加边的数目+1if (count == vexnum - 1)    // 达到要求则退出即可break;}cout << all_path << endl;}

最小生成树(Prim算法和Kruskal算法)相关推荐

  1. 最小生成树-Prim算法和Kruskal算法

    转载自:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html Prim算法 1.概览 普里姆算法(Prim算法),图论 ...

  2. 数据结构与算法—最小生成树(Prim算法和Kruskal算法算法详解)

    前言 在数据结构与算法的图论中,(生成)最小生成树算法是一种常用并且和生活贴切比较近的一种算法.但是可能很多人对概念不是很清楚.我们看下百度百科对于最小生成树定义: 一个有 n 个结点的连通图的生成树 ...

  3. 生成随机数放入整型数组怎么判断有没有重复_图的应用(1)-连通图的最小生成树(Prim算法和Kruskal算法)...

    连通图的生成树: 是一个极小的连通图,它含有图中全部的N个顶点,但是只足以构成一颗树的N-1条边. 必须满足三个条件: 图是连通图: 图中包含了N个结点 图中边的数量等于N-1条. 连通图生成树的判断 ...

  4. 最小生成树———prim算法和kruskal算法详解

    最小生成树之prim算法(转载出处) 边赋以权值的图称为网或带权图,带权图的生成树也是带权的,生成树T各边的权值总和称为该树的权. 最小生成树(MST):权值最小的生成树. 生成树和最小生成树的应用: ...

  5. 最小生成树Prim算法和Kruskal算法

    https://www.cnblogs.com/JoshuaMK/p/prim_kruskal.html 转载于:https://www.cnblogs.com/DixinFan/p/9225105. ...

  6. 【最小生成树】Prim算法和Kruskal算法的区别对比

    Prim算法和Kruskal算法都是从连通图中找出最小生成树的经典算法- 从策略上来说,Prim算法是直接查找,多次寻找邻边的权重最小值,而Kruskal是需要先对权重排序后查找的- 所以说,Krus ...

  7. 加权无向图与最小生成树(Prim算法和Kruskal算法)

    目录 0 引入 1 图的最小生成树定义及相关约定 2 最小生成树原理 2.1 性质 2.2 切分定理 3 贪心思想 4 Prim算法 4.1 算法步骤 4.2 API设计 4.3 Java代码演示 5 ...

  8. 求的带权图最小生成树的Prim算法和Kruskal算法

    求的带权图最小生成树的Prim算法和Kruskal算法 最小生成树的概念 最小生成树其实是最小权重生成树的简称. 一个连通图可能有多个生成树.当图中的边具有权值时,总会有一个生成树的边的权值之和小于或 ...

  9. 作业1-采用Prim算法和Kruskal算法构造最小生成树

    采用Prim算法和Kruskal算法构造最小生成树 实验报告 1.问题 2.解析 (1)Prim算法 (2)Kruskal算法 3.设计 (1)Prim算法 (2)Kruskal算法 4.分析 (1) ...

  10. 最小生成树之Prim算法和Kruskal算法

    一个连通图可能有多棵生成树,而最小生成树是一副连通加权无向图中一颗权值最小的生成树,它可以根据Prim算法和Kruskal算法得出,这两个算法分别从点和边的角度来解决. Prim算法 输入:一个加权连 ...

最新文章

  1. 25个让Java程序员更高效的Eclipse插件
  2. centos启动流程
  3. 拼接字符SQL语句拼接 最后一个字符多出 处理方式
  4. 趣图:脸部识别最快的实现
  5. 阿里云服务器部署Java Web项目全过程
  6. 立于山巅!他,凭什么抗住万亿级流量冲击!
  7. 修改PyCharm主题、字体大小、汉化PyCharm、安装translation翻译插件
  8. Qt配置OpenCV教程,无需复杂的编译过程,(详细版)
  9. 北京2018积分落户名单
  10. 虚拟机安装win10(ghost镜像)
  11. 《朝歌封神录》10.22正式上线链游玩家|山海异闻、奇幻仙侠
  12. dota2api的介绍与使用
  13. freemarker制作word模板
  14. 【ARM嵌入式】——多寄存器寻址
  15. sql日期中文大写显示
  16. STM32F407VG晶振与主频配置
  17. 简单理解椭圆曲线的非对称加密应用
  18. MXNet:基础和入门
  19. 综合练习Java算法
  20. 小程序 - K线图画法

热门文章

  1. 新西兰 计算机 转专业,在留学新西兰以后,半途想转专业怎么办?
  2. 教育大数据可视化研究综述笔记
  3. U3D常用介绍,搭建一个简单的三维效果
  4. 独立t检验和配对t检验_配对学生的t检验是什么?
  5. 如何正确地跟二维码里的神仙打架?给小朋友讲解二维码原理
  6. 【损失函数】生成任务感知损失小结
  7. YOLOV5 网络模块解析
  8. 树莓派学习笔记(通过网线连接)
  9. [CTF]-NepCTF2022
  10. 判断PPC或者SP平台