浅入浅出数据结构(25)——最小生成树问题

  上一篇博文我们提到了图的最短路径问题:http://www.cnblogs.com/mm93/p/8434056.html。而最短路径问题可以说是这样的一个问题:路已经修好了,该怎么从这儿走到那儿?但是在和图有关的问题中,还有另一种有趣的问题:修路的成本已经知道了,该怎么修路才能尽可能节约成本,同时将这些地方都连起来?

  比如我们知道有这么几个城市,它们互相之间还没有路:

  

  经过实地考察后,发现可以修的路以及各条路的修路成本如下:

  

  但是我们的预算有限,需要在修路时尽可能的省钱(也就是尽量减小所有边的权重之和),同时保证图中每一个城市总是能到达图中任意一个城市,该怎么修路呢?对于上图来说,其中一个方案是这样的,其总共的修路成本(即总权重)为8:

  

  另一个方案是这样的,略有不同,不过总成本也是8:

  

  像这样的问题,就是我们今天要讨论的最小生成树问题。为了更准确地说明什么是最小生成树,我们需要先了解一个概念:连通。对于一个无向图而言,如果每个顶点到每个其它顶点都存在路径,则该无向图是连通的。而对于有向图而言,道理相同又稍有变化,在有向图中,若每个顶点到每个其它顶点都存在可行的路径,则该有向图是强连通的。比如下图就不是一个强连通的有向图,其中非v0顶点无法到达v0顶点:

  

  但是如果我们将上面这个有向图的边都变为无向边,我们就会得到一个无向图,此无向图即该有向图的基础图(underlying graph)。如果一个有向图非强连通,但是其基础图是连通的,我们就称该有向图是弱连通的。上面这个有向图就是一个弱连通的有向图。

  明白了什么是连通之后,接下来我们说说最小生成树是什么:在一个连通的无向图的所有边中,挑选出足以使所有顶点连通的那些边,且这些边的总权重不能更低,则这些边与所有顶点构成的图就是最小生成树。“最小”的意思是其总权重是最小的,“生成”则是因为这个树是从一个无向图中找出来的,也即生成的。

  等等_(:з」∠)_  不是说“这些边与所有顶点构成的图”吗,怎么就成了树?原因是这样的,如果一个无向图是连通的,那么我们就能找出满足上述条件的那个图,而如果那个图存在,那它一定是一棵树(树是特殊的图嘛,这一点应该要懂的),比如本文前面所找出的最小生成图,显然是一棵树:

  

  为什么称最后找出来的顶点与边的集合为最小生成树,我们已经知道了,而为什么最后找出来的一定是树……咱能不纠结吗 ( ̄. ̄)

  好了,接下来讨论下一个问题:有向图可以找出最小生成树吗?答案是可以,只要有向图是强连通的。并且寻找有向图的最小生成树的过程也是基本一样的,因为无向图本就是以有向图的形式存储的(一条无向边拆成两条有向边)。不过因为本文并不打算给出可运行的代码,所以我们的讨论以无向图为基准,主要关注算法的思路,并且不考虑所给图非连通的情况。

  想要在图中找出最小生成树,有两种算法可供选择:Prim算法和Kruskal算法。因为Prim算法与寻找最短路径的Dijkstra算法非常非常非常像,所以我们先来讨论一下Prim算法。

  Prim算法的思路是这样的:

  1.任选一个顶点,将其标为已知,即表示该顶点已在树中(Dijkstra算法中,起点由我们指定)

  2.找出所有已知顶点邻接的未知顶点,其中与任一已知顶点的邻接边权重最小的未知顶点,我们将其标为已知,同时将其preV设为与其邻接边最小的已知顶点,且其distance设为该邻接边的权重(在Dijkstra算法中,我们用的是“指向”,因为要考虑到有向图的情况,此外,Dijkstra算法中,我们将被标为已知的未知顶点的distance设为与其相连的已知顶点的distance加上边的权重)

  3.反复执行第二步,直至不存在已知顶点邻接了未知顶点为止。

  抽象的说,Prim算法就是随机选一个顶点,将其拉进原先为空的树中,然后不断地通过尽可能小的边将其他顶点拉进这棵树中

  

  老样子,上述说法晦涩难懂 ( ̄. ̄)。所以我们需要实际的走一遍来加深一下理解,以下图为例:

  

  假设我们以v3作为起点,则图初始化后的状态如下(顶点旁有红圈表示该顶点已知,红圈中即该顶点的preV,顶点的distance我们暂不考虑):

  

  接着,我们找出所有已知顶点(v3)邻接的所有未知顶点:v0、v1、v2、v4、v5、v6。发现与已知顶点邻接边最小的未知顶点是v1、v4,其中未知顶点v1与已知顶点v3的邻接边权重为1,未知顶点v4与已知顶点v3的邻接边权重也为1,我们任选其一即可,比如选择v1,然后将v1设为已知,v1.preV=v3:

  

  继续,我们找出所有已知顶点(v1、v3)邻接的所有未知顶点:v0、v2、v4、v5、v6,发现与已知顶点邻接边最小的未知顶点是v0、v4,其中未知顶点v0与已知顶点v1的邻接边权重为1,未知顶点v4与已知顶点v1或v3的邻接边权重为1,我们任选其一,比如v4,然后将v4设为已知,v4.preV=v1(也可以是v4.preV=v3):

  

  继续,我们找出所有已知顶点(v1、v3、v4)邻接的所有未知顶点:v0、v2、v5、v6,发现与已知顶点邻接边最小的未知顶点是v0、v6,其中未知顶点v0与已知顶点v1的邻接边权重为1,未知顶点v6与已知顶点v4的邻接边权重为1,我们选v6,将v6设为已知,v6.preV=v4:

  

  继续,我们找出所有已知顶点邻接的所有未知顶点:v0、v2、v5,其中与已知顶点的邻接边最小的是v0,未知顶点v0与已知顶点v1的邻接边权重为1,我们将v0设为已知,v0.preV=v1:

  

  继续,找出所有已知顶点邻接的所有未知顶点:v2、v5,发现其中未知顶点v5与已知顶点v6的邻接边权重最小为2,所以我们将v5设为已知,v5.preV=v6:

  

  继续,找出所有已知顶点邻接的所有未知顶点:v2,其中未知顶点v2与已知顶点v5的邻接边权重最小,所以我们将v2设为已知,v2.preV=v5:

  

  继续,发现已经没有哪个已知顶点邻接了未知顶点,所以算法结束。

  接下来,我们只要进行这两步操作就可以得出最小生成树:

  1.将每个顶点与其preV相连的边标为已知

  2.将非已知的边删去。

  将每个顶点与其preV相连的边标为已知(注意,v3的preV是自身,此情况我们不做任何操作即可):

  

  删去非已知的边:

  

  当然,在实际编程中,有可能并不会执行这两个操作,我们只要在将最小生成树的相关信息保存在pathTable中即可,本例中算法结束后pathTable应为如下(与Dijkstra算法使用的pathTable略有不同:没有distance域):

  

  当然,我们也可以使用和Dijkstra算法时一样的pathTable,即加上distance域,不过在计算最小生成树时,一个顶点的distance域应该是其与preV邻接的边的权重:

  

  在我们走一遍Prim算法时,我们发现v4.preV既可以设为v3,也可以设为v1,这就已经说明了一点:一个图的最小生成树并不一定是唯一的。不过还要注意的是:一个图即便有多个最小生成树,它们的总权重也应该是一样的。

  如果你回顾一遍Prim算法和Dijkstra算法,就会发现,Prim算法与Dijkstra算法的区别可以说就两个:

  1.Prim算法的“起点”是任选的,Dijkstra算法是给定的(毕竟要找的是单源最短路径)

  2.Prim算法在将一个未知顶点设为已知时,其distance设为其与已知顶点的最小邻接边的weight,而Dijkstra算法则是设为已知顶点.distance+weight

  换句话说,Prim算法就是稍稍修改了一下的Dijkstra算法。如果你仔细观察我们用Prim算法生成的树,你会发现从v3出发到任意顶点的路径恰好是v3到该顶点的最短路径:

  

  

  接下来本应讨论Kruskal算法,但是我忽然发现我之前忘了写一篇关于树的集合与不相交集的博文(⊙?⊙)。如果要讨论Kruskal算法,这两个预备知识是必不可少的,而如果这两个知识也要讲解的话,博文就太长了Orz。所以我只简单说说Kruskal的思路。

  在Prim算法中,我们是以已知顶点(即已在最小生成树中的顶点)为基础,不断地将未知顶点拉进树中。而Kruskal算法则是另一种思路:以当前最小的边为基础,不断地将未知顶点拉进树中(这个过程可能产生多棵树)。

  以下图为例,我们走一遍Kruskal算法:

  

  首先,我们需要将边按权重从小到大排序,才能找“当前最小边”:

  

  先是处理当前最小边[v0,v1],其所连接的两个顶点均未知,所以我们将它们均设为已知,并连起来:

  

  然后处理下一个当前最小边[v1,v3],其所连接的v3未知,将v3设为已知,连起来:

  

  接着处理边[v3,v4],其连接的v4未知,将v4设为已知,连起来:

  

  接着处理边[v1,v4],其连接的两个顶点均为已知,故跳过

  接着处理边[v4,v6],其连接的v6未知,将v6设为已知,连起来:

  

  接着处理边[v2,v5],其连接的v2和v5均为未知,所以将v2、v5均设为已知,连起来(注意,此时产生了两棵树):

  

  接着处理边[v5,v6],其连接的顶点均为已知,但是v5和v6处于不同的树,所以我们将其连起来(这部分的相关判断和处理需要树的集合知识,以及不相交集数据结构):

  

  接下来处理的所有边都是所连接顶点已知,且所连接顶点处于同一棵树中,所以均会跳过,然后算法结束。

  没有掌握住博文的顺序和铺垫,实在是失败 ̄△ ̄

  不过Kruskal算法的思路我想我应该讲清楚了,就是Dijkstra算法和Prim算法的讲解可能太生硬了一些,但是细细地读、细细地理解、细细地过一遍,应该还是能明白的?ω?

  这个系列的博文的主体部分到这儿就算结束了,从第一篇博文一路看到这儿的话,基本的数据结构和算法应该都能掌握。而像什么B+树、红黑树、算法设计技巧等更特殊的知识我没有算在主体部分中,以后可能会以“浅入浅出数据结构(附)”的标题形式写出。

原文地址:https://www.cnblogs.com/mm93/p/8437939.html

时间: 2024-08-02 09:31:51

浅入浅出数据结构(25)——最小生成树问题的相关文章

浅入深出之Java集合框架(下)

Java中的集合框架(下) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,哈哈这篇其实也还是基础,惊不惊喜意不意外 ̄▽ ̄ 写文真的好累,懒得写了.. 温馨提醒:建议从(上)开始看哦~ 目 录 浅入深出之Java集合框架(上) 浅入深出之Java集合框架(中)   浅入深出之Java集合框架(下) 前 言 在<浅入深出之Java集合框架(中) >中介绍了Map接口的基本操作.使用的示例是在<浅入深出之Java集合框架(上)>中的模拟学

浅入深出之Java集合框架(上)

Java中的集合框架(上) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到<浅入深出之Java集合框架(下)>. 目录: 浅入深出之Java集合框架(上) 浅入深出之Java集合框架(中)   努力赶制中..关注后更新会提醒哦! 浅入深出之Java集合框架(下) 努力赶制中..关注后更新会提醒哦! 一.集合概述 1)集合的概念 现实生活中的集合:很多事物凑在一起. 数学中的集合:具有共同属性的事物的总体

『浅入浅出』MySQL 和 InnoDB

作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL.PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系的认知,所以最近两个月的时间看了几本数据库相关的书籍并且阅读了 MySQL 的官方文档,希望对各位了解数据库的.不了解数据库的有所帮助. 本文中对于数据库的介绍以及研究都是在 MySQL 上进行的,如果涉及到了其他数据库的内容或者实现会在文中单独指出. 数据库的定义 很多开发者在最开始时其实都对数据

重新学习MySQL数据库2:『浅入浅出』MySQL 和 InnoDB

重新学习Mysql数据库2:『浅入浅出』MySQL 和 InnoDB 作为一名开发人员,在日常的工作中会难以避免地接触到数据库,无论是基于文件的 sqlite 还是工程上使用非常广泛的 MySQL.PostgreSQL,但是一直以来也没有对数据库有一个非常清晰并且成体系的认知,所以最近两个月的时间看了几本数据库相关的书籍并且阅读了 MySQL 的官方文档,希望对各位了解数据库的.不了解数据库的有所帮助. 本文中对于数据库的介绍以及研究都是在 MySQL 上进行的,如果涉及到了其他数据库的内容或者

浅入深出ElasticSearch构建高性能搜索架构

浅入深出ElasticSearch构建高性能搜索架构  课程学习地址:http://www.xuetuwuyou.com/course/161 课程出自学途无忧网:http://www.xuetuwuyou.com 一.课程用到的软件 ElasticSearch5.0.0 Spring Tool Suite 3.8.2.RELEASE Maven3.0.5 Spring4 Netty4 Hadoop2.7.1 Kibana5.0 JDK1.8.0_111 二.课程目标 1.快速学习Elastic

浅入浅出EmguCv(三)EmguCv打开指定视频

打开视频的思路跟打开图片的思路是一样的,只不过视频是由一帧帧图片组成,因此,打开视频的处理程序有一个连续的获取图片并逐帧显示的处理过程.GUI同<浅入浅出EmguCv(二)EmguCv打开指定图片>一样,只不过处理程序编程如下所示: 1 /// <summary> 2 /// 点击按钮打开指定图片 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <pa

浅入浅出 Go 语言接口的原理

浅入浅出 Go 语言接口的原理 接口是 Go 语言的重要组成部分,它在 Go 语言中通过一组方法指定了一个对象的行为,接口 interface 的引入能够让我们在 Go 语言更好地组织并写出易于测试的代码.然而很多使用 Go 语言的工程师其实对接口的了解都非常有限,对于它的底层实现也一无所知,这其实成为了我们使用和理解 interface 的最大阻碍. 在这一节中,我们就会介绍 Go 语言中这个重要类型 interface 的一些常见问题以及它底层的实现,包括接口的基本原理.类型断言和转换的过程

包学会之浅入浅出Vue.js:升学篇

上一篇<包学会之浅入浅出Vue.js:开学篇>中,我们初步了解单页面组件这个概念,现在通过一个项目,来进一步解析组件的应用吧,Go~ 需求背景 组件库是做UI和前端日常需求中经常用到的,把一个按钮,导航,列表之类的元素封装起来,方便日常使用,调用方法只需直接写上<qui-button></qui-button>或者<qui-nav></qui-nav>这样的代码就可以,是不是很方便呢,接下来我们将要完成以下页面: 这是我们组件库的首页,包含三个子

浅入浅出SQL注入

已经开始了学习牛腩新闻发布系统,在讲后台代码的时候讲了一些重构SQLHelper的知识,存储过程和触发器等,这些以前都是接触过的.而SQL注入是以前没有注意过的,所以停下来总结学习一下SQL注入. 首先什么是SQL注入呢? 实战篇~~~~~~~~~~ SQL注入概念 所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令,比如先前的很多影视网站泄露VIP会员密码大多就是通过WEB表单递交查询字符暴出的,这类表单特别容易受到