本文始发于个人公众号:TechFlow,原创不易,求个关注

今天是算法和数据结构专题20篇文章,我们继续最小生成树算法,来把它说完。

在上一篇文章当中,我们主要学习了最小生成树的Kruskal算法。今天我们来学习一下Prim算法,来从另一个角度来理解一下这个问题。

从边到点

我们简单回顾一下Kruskal算法的原理,虽然上篇文章当中用了很多篇幅,但是原理非常简单。本质上就是我们对图中所有的边按照长度进行排序,之后我们按照顺序依次把它作为树的骨干,加入到树上来。

在此过程当中,我们为了避免导致产生环,而破坏树结构,所以使用了并查集算法来作为维护。只会考虑那些不在一个连通块中的边,否则就会构成环路。

很多人在学习了这个算法之后,会将它理解成贪心问题,或者是并查集的一个使用场景。这么理解倒也没错,但是在这个问题当中,还有更好的解释。

这个解释就是边集的扩张

整个Kruskal运行的过程是我们不断选择边加入树中的过程,对于n个点的图来说,我们需要n-1条边。如果我们专注于这个被选出来边的集合,那么算法开始的时候,它是空集,运行结束之后,它含有n-1条边,达到饱和。

当然你也可以换个角度来看,如果我们的关注点在点上,那么最小生成树构建的过程也同样可以看成是点集的拓张。只不过点和边不同,边可以选择,但是点不可以,点只能通过选择边来覆盖。比如我们看下下图:

在图中,我们左边是一棵已经构成的树,当我们连通AE之后,我们就用边覆盖了E点。点是依托于边的,不通过边,是无法覆盖点的。

我们从空集开始,除了第一条边可以覆盖两个点之外,之后的每一条边都连通一个已经覆盖的点和一个没有覆盖的点。那么,同样也是通过n-1条边可以覆盖n个点。这个就是Prim算法的核心思路,也就是点集的拓张。

整体思路

我们明白了Prim的核心思想是点集的拓张之后就容易了,由于我们每次选择的边两边一定是一个已经覆盖的点和没有覆盖的点。所以我们生成的树是一条边一条边逐渐长大的,而不是像Kruskal那样东拼西凑起来的。

我们来看个例子:

我们已经连通了ABCD四个点,其中CE的长度是7,DF的长度是9,EF的长度是5。虽然EF的长度小于CE,但是由于我们必须要连通一个已经覆盖的点和没有覆盖的点,虽然EF的距离更小,我们也不能选择。只能选择CE,所以在整个算法运行的过程中间,这棵树是逐渐变大的。如果是Kruskal,我们肯定会先连通EF,再连通CE,整个算法运行的过程当中,各个部分都是隔开的,最后的树其实是逐渐“拼凑”出来的。

和Kruskal维护集合相比,我们维护点有没有覆盖过则要容易得多。因为树已经选择的边是不会修改的,所以我们只需要用一个数组标记一下每个位置的点有没有覆盖即可。简单的bool类型就可以实现,非常方便。

所以我们的问题只剩下了一个,如何保证我们生成出来的树的路径和最小呢?

关于这个问题的回答Prim和Kruskal一样,就是贪心。我们每次选择最小的边进行拓展,Kruskal是对所有边进行排序,然后依次判断能否选择。那么Prim算法怎么用贪心呢?

其实也很简单,我们也很容易想明白。Prim算法对边有限制,只能选择已经覆盖的点和没有覆盖的点之间的连边。我们给这些边起个名字,叫做可增广边。那么,显然我们要做的就是在可增广边当中选择一条最短的进行增广。

问题就只剩下了一个,我们怎么选择和维护这个最短的可增广边呢,难道每次拓充之后,都进行排序吗?

显然不是,因为每次都排序带来的开销太大了,我们可以用一个数据结构来维护这些边,让它们按照边的长度进行排序。这个数据结构我们应该很熟悉了,就是我们已经遇见过好几次的——优先队列

我们排序的键也已经很明显了,就是边的长度,边是否合法的判断也很简单,我们只要判断一下是否存在没有覆盖的点即可。于是整个流程就串起来了,我们可以先来把流程理一下,写出它的流程:

选择一个点u,当做已经覆盖
把u所有相连的边加入队列
循环
循环 从队列头部弹出边
如果边合法
弹出
跳出循环
获取边的两个端点
将未覆盖的端点所有边加入队列
直到所有点都已经覆盖

最后,我们看下Python的实现,首先是优先队列的部分,这个逻辑我们可以利用现成的heapq来实现。

import heapq

class PriorityQueue:

    def __init__(self):
self._queue = []
self._index = 0 def push(self, item, priority):
# 传入两个参数,一个是存放元素的数组,另一个是要存储的元素,这里是一个元组。
# heap内部默认从小到大排
heapq.heappush(self._queue, (priority, self._index, item))
self._index += 1 def pop(self):
return heapq.heappop(self._queue)[-1] def empty(self):
return len(self._queue) == 0

然后是Prim算法的实现,这里为了存储方便,我们使用了邻接表来存储边的信息。邻接表其实是一个链表的数组,数组里的每一个元素都是一个链表的头结点。这个链表存储的是某一个节点的所有边信息。

比如邻接表中下标1的链表存储的就是与1这个节点相连的所有边的信息。这个数据结构在我们存储树和图的时候经常用到,不过也并不复杂,我们也不用真的实现一个链表,因为可以通过数组来模拟。

edges = [[1, 2, 7], [2, 3, 8], [2, 4, 9], [1, 4, 5], [3, 5, 5], [2, 5, 7], [4, 5, 15], [4, 6, 6], [5, 6, 8], [6, 7, 11], [5, 7, 9]]

if __name__ == "__main__":
# 记录点是否覆盖
visited = [False for _ in range(11)]
visited[1] = True
# 邻接表,可以理解成二维数组
adj_table = [[] for _ in range(11)]
# u和v表示两个端点,w表示线段长度
# 我们把v和w放入下标u中
# 把u和w放入下标v中
for (u, v, w) in edges:
adj_table[u].append([v, w])
adj_table[v].append([u, w]) que = PriorityQueue() # 我们选择1作为起始点
# 将与1相邻的所有边加入队列
for edge in adj_table[1]:
que.push(edge, edge[1]) ret = 0
# 一共有7个点,我们需要加入6条边
for i in range(7):
# 如果队列为空,说明无法构成树
while not que.empty():
u, w = que.pop()
# 如果连通的端点已经被覆盖了,则跳过
if visited[u]:
continue
# 标记成已覆盖
visited[u] = True
ret += w
# 把与它相连的所有边加入队列
for edge in adj_table[u]:
que.push(edge, edge[1])
break print(ret)

结尾

到这里,关于Prim算法的介绍就结束了。其实本质上来说Prim和Kruskal是最小生成树算法的一体两面,两者的本质都是一样的,就是增广。只不过不同的是,两者一个是点的增广一个是边的增广而已。但是由于点的增广也依托于边,所以Prim当中既用到点来判断是否覆盖,又用到边的信息来增广点。

如果单纯从算法逻辑入手,没有能够理解它的本质,不仅很容易把这两个算法搞混淆,也容易在写代码的时候搞晕,不知道到底要维护什么,要拓展什么。

增广的思想在图论相关的算法当中经常用到(比如网络流),并不只是在最小生成树当中出现,因此理解这一概念对于我们后续的学习非常重要。希望大家都能领会其中的精髓。

今天的文章就到这里,原创不易,扫码关注我,获取更多精彩文章。

最小生成树的本质是什么?Prim算法道破天机的更多相关文章

  1. hiho一下 第二十九周 最小生成树三·堆优化的Prim算法【14年寒假弄了好长时间没搞懂的prim优化:prim算法+堆优化 】

    题目1 : 最小生成树三·堆优化的Prim算法 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 回到两个星期之前,在成功的使用Kruscal算法解决了问题之后,小Ho产生 ...

  2. 最小生成树 (Minimum Spanning Tree,MST) --- Prim算法

    本文链接:http://www.cnblogs.com/Ash-ly/p/5409904.html 普瑞姆(Prim)算法: 假设N = (V, {E})是连通网,TE是N上最小生成树边的集合,U是是 ...

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

    依据图的深度优先遍历和广度优先遍历,能够用最少的边连接全部的顶点,并且不会形成回路. 这样的连接全部顶点并且路径唯一的树型结构称为生成树或扩展树.实际中.希望产生的生成树的全部边的权值和最小,称之为最 ...

  4. JS实现最小生成树之普里姆(Prim)算法

    最小生成树: 我们把构造连通网的最小代价生成树称为最小生成树.经典的算法有两种,普利姆算法和克鲁斯卡尔算法. 普里姆算法打印最小生成树: 先选择一个点,把该顶点的边加入数组,再按照权值最小的原则选边, ...

  5. hihoCoder#1109 最小生成树三·堆优化的Prim算法

    原题地址 坑了我好久...提交总是WA,找了个AC代码,然后做同步随机数据diff测试,结果发现数据量小的时候,测试几十万组随机数据都没问题,但是数据量大了以后就会不同,思前想后就是不知道算法写得有什 ...

  6. 数据结构:最小生成树--Prim算法

    最小生成树:Prim算法 最小生成树 给定一无向带权图.顶点数是n,要使图连通仅仅需n-1条边.若这n-1条边的权值和最小,则称有这n个顶点和n-1条边构成了图的最小生成树(minimum-cost ...

  7. 最小生成树——Kruskal与Prim算法

    最小生成树——Kruskal与Prim算法 序: 首先: 啥是最小生成树??? 咳咳... 如图: 在一个有n个点的无向连通图中,选取n-1条边使得这个图变成一棵树.这就叫“生成树”.(如下图) 每个 ...

  8. 算法对比:Prim算法与Dijskra算法

    在图论中,求MST的Prim算法和求最短路的Dijskra算法非常像.可是我一直都对这两个算法处于要懂不懂的状态,现在,就来总结一下这两个算法. 最小生成树(MST)—Prim算法: 算法步骤: •将 ...

  9. Algorithm --> Kruskal算法和Prim算法

    最小生成树之Kruskal算法和Prim算法 Kruskal多用于稀疏图,prim多用于稠密图. 根据图的深度优先遍历和广度优先遍历,可以用最少的边连接所有的顶点,而且不会形成回路.这种连接所有顶点并 ...

随机推荐

  1. powershell提示无法将“”项识别

    解决: 完成! 解释: 权限问题.Powershell脚本的4种执行权限介绍,Windows默认不允许任何脚本运行,我们可以使用"Set-ExecutionPolicy"cmdle ...

  2. fasttext 和pysparnn的安装

  3. Python 输出 log 到文件的方法

    import loggingfrom logging.handlers import RotatingFileHandler module_name = "test_module" ...

  4. [Batch脚本] if else 的格式

    必须写成一行 ) else (,否则报错. if %abc%=="yes" ( ... ) else ( ... )

  5. ThinkJS前端搭配vue时的Nginx配置

    Thinkjs 作为奇舞团开源的nodejs mvc框架之一,引起了很多NodeJS程序员的亲赖.但是其关于静态文件处理部分支持不够完善,主要是体现在SPA单页应用,之前在ThinkJS 2.*版本时 ...

  6. 对于WebP格式入门解读

    因为项目中需要用到大量动画效果,前期尝试过几种方案,比如GIF.帧动画.lottie.SVGA等格式的动画渲染方案,发现都存在各式各样的问题.比如: 1,GIF格式.5秒的动画,一张图大小可能就会达到 ...

  7. Scala教程之:函数式的Scala

    文章目录 高阶函数 强制转换方法为函数 方法嵌套 多参数列表 样例类 比较 拷贝 模式匹配 密封类 单例对象 伴生对象 正则表达式模式 For表达式 Scala是一门函数式语言,接下来我们会讲一下几个 ...

  8. Linux中的常用符号

    >, 1>     输出重定向符stdout,代码为1,重定向内容到文件,清除已有的内容,然后加入新内容,如果文件不存在还会创建文件 >>, 1>>   追加输出重 ...

  9. 解决material UI中弹窗(dialog、popover等)内容被遮挡问题

    在material ui中有几种弹出层,比如:dialog.popover等,这些弹出层都会遇到的一个公共问题是: 假如弹出层中的内容变化了,弹出层的位置并不会重新定位. 这样,假如一开始弹出层定位在 ...

  10. Javascript基础之-var,let和const深入解析(二)

    你想在在变量声明之前就使用变量?以后再也别这样做了. 新的声明方式(let,const)较之之前的声明方式(var),还有一个区别,就是新的方式不允许在变量声明之前就使用该变量,但是var是可以得.请 ...