位置: 编程技术 - 正文

Python算法之图的遍历(python图论算法)

编辑:rootadmin

推荐整理分享Python算法之图的遍历(python图论算法),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:python算法图解 pdf,python图解,python图解,python图论算法,python 图算法,python图论算法,python 图算法,python图算法库,内容如对您有帮助,希望把文章链接给更多的朋友!

本节主要介绍图的遍历算法BFS和DFS,以及寻找图的(强)连通分量的算法

Traversal就是遍历,主要是对图的遍历,也就是遍历图中的每个节点。对一个节点的遍历有两个阶段,首先是发现(discover),然后是访问(visit)。遍历的重要性自然不必说,图中有几个算法和遍历没有关系?!

[算法导论对于发现和访问区别的非常明显,对图的算法讲解地特别好,在遍历节点的时候给节点标注它的发现节点时间d[v]和结束访问时间f[v],然后由这些时间的一些规律得到了不少实用的定理,本节后面介绍了部分内容,感兴趣不妨阅读下算法导论原书]

图的连通分量是图的一个最大子图,在这个子图中任何两个节点之间都是相互可达的(忽略边的方向)。我们本节的重点就是想想怎么找到一个图的连通分量呢?

一个很明显的想法是,我们从一个顶点出发,沿着边一直走,慢慢地扩大子图,直到子图不能再扩大了停止,我们就得到了一个连通分量对吧,我们怎么确定我们真的是找到了一个完整的连通分量呢?可以看下作者给出的解释,类似上节的Induction,我们思考从i-1到i的过程,只要我们保证增加了这个节点后子图仍然是连通的就对了。

Let'slookatthefollowingrelatedproblem.Showthatyoucanorderthenodesinaconnectedgraph,V1,V2,…Vn,sothatforanyi=1…n,thesubgraphoverV1,…,Viisconnected.Ifwecanshowthisandwecanfigureouthowtodotheordering,wecangothroughallthenodesinaconnectedcomponentandknowwhenthey'reallusedup.

Howdowedothis&#;Thinkinginductively,weneedtogetfromi-1toi.Weknowthatthesubgraphoverthei-1firstnodesisconnected.Whatnext&#;Well,becausetherearepathsbetweenanypairofnodes,consideranodeuinthefirsti-1nodesandanodevintheremainder.Onthepathfromutov,considerthelastnodethatisinthecomponentwe'vebuiltsofar,aswellasthefirstnodeoutsideit.Let'scallthemxandy.Clearlytheremustbeanedgebetweenthem,soaddingytothenodesofourgrowingcomponentkeepsitconnected,andwe'veshownwhatwesetouttoshow.

经过上面的一番思考,我们就知道了如何找连通分量:从一个顶点开始,沿着它的边找到其他的节点(或者说站在这个节点上看,看能够发现哪些节点),然后就是不断地向已有的连通分量中添加节点,使得连通分量内部依然满足连通性质。如果我们按照上面的思路一直做下去,我们就得到了一棵树,一棵遍历树,它也是我们遍历的分量的一棵生成树。在具体实现这个算法时,我们要记录“边缘节点”,也就是那些和已得到的连通分量中的节点相连的节点,它们就像是一个个待办事项(to-dolist)一样,而前面加入的节点就是标记为已完成的(checkedoff)待办事项。

这里作者举了一个很有意思的例子,一个角色扮演的游戏,如下图所示,我们可以将房间看作是节点,将房间的门看作是节点之间的边,走过的轨迹就是遍历树。这么看的话,房间就分成了三种:(1)我们已经经过的房间;(2)我们已经经过的房间附近的房间,也就是马上可以进入的房间;(3)“黑屋”,我们甚至都不知道它们是否存在,存在的话也不知道在哪里。

根据上面的分析可以写出下面的遍历函数walk,其中参数S暂时没有用,它在后面求强连通分量时需要,表示的是一个“禁区”(forbidden zone),也就是不要去访问这些节点。

注意下面的difference函数的使用,参数可以是多个,也就是说调用后返回的集合中的元素在各个参数中都不存在,此外,参数也不一定是set,也可以是dict或者list,只要是可迭代的(iterables)即可。可以看下python docs

我们可以用下面代码来测试下,得到的结果没有问题

上面的walk函数只适用于无向图,而且只能找到一个从参数s出发的连通分量,要想得到全部的连通分量需要修改下

用下面的代码来测试下,得到的结果没有问题

至此我们就完成了一个时间复杂度为Θ(E+V)的求无向图的连通分量的算法,因为每条边和每个顶点都要访问一次。[这个时间复杂度会经常看到,例如拓扑排序,强连通分量都是它]

[接下来作者作为扩展介绍了欧拉回路和哈密顿回路:前者是经过图中的所有边一次,然后回到起点;后者是经过图中的所有顶点一次,然后回到起点。网上资料甚多,感兴趣自行了解]

下面我们看下迷宫问题,如下图所示,原始问题是一个人在公园中走路,结果走不出来了,即使是按照“左手准则”(也就是但凡遇到交叉口一直向左转)走下去,如果走着走着回到了原来的起点,那么就会陷入无限的循环中!有意思的是,左边的迷宫可以通过“左手准则”转换成右边的树型结构。

[注:具体的转换方式我还未明白,下面是作者给出的构造说明]

Here the “keep one hand on the wall” strategy will work nicely. One way of seeing why it works is to observe that the maze really has only one inner wall (or, to put it another way, if you put wallpaper inside it, you could use one continuous strip). Look at the outer square. As long as you're not allowed to create cycles, any obstacles you draw have to be connected to the it in exactly one place, and this doesn't create any problems for the left-hand rule. Following this traversal strategy, you'll discover all nodes and walk every passage twice (once in either direction).

上面的迷宫实际上就是为了引出深度优先搜索(DFS),每次到了一个交叉口的时候,可能我们可以向左走,也可以向右走,选择是有不少,但是我们要向一直走下去的话就只能选择其中的一个方向,如果我们发现这个方向走不出去的话,我们就回溯回来,选择一个刚才没选过的方向继续尝试下去。

基于上面的想法可以写出下面递归版本的DFS

很自然的我们想到要将递归版本改成迭代版本的,下面的代码中使用了Python中的yield关键字,具体的用法可以看下这里IBM Developer Works

上面迭代版本经过一点点的修改可以得到更加通用的遍历函数

函数traverse中的参数qtype表示队列类型,例如栈stack,下面的代码给出了如何自定义一个stack,以及测试traverse函数

如果还不清楚的话可以看下算法导论中的这幅DFS示例图,节点的颜色后面有介绍

上图在DFS时给节点加上了时间戳,这有什么作用呢?

前面提到过,在遍历节点的时候如果给节点标注它的发现节点时间d[v]和结束访问时间f[v]的话,从这些时间我们就能够发现一些信息,比如下图,(a)是图的一个DFS遍历加上时间戳后的结果;(b)是如果给每个节点的d[v]到f[v]区间加上一个括号的话,可以看出在DFS遍历中(也就是后来的深度优先树/森林)中所有的节点 u 的后继节点 v 的区间都在节点 u 的区间内部,如果节点 v 不是节点 u 的后继,那么两个节点的区间不相交,这就是“括号定理”。

加上时间戳的DFS遍历还算比较好写对吧

除了给节点加上时间戳之外,算法导论在介绍DFS的时候还给节点进行着色,在节点被发现之前是白色的,在发现之后先是灰色的,在结束访问之后才是黑色的,详细的流程可以参考上面给出的算法导论中的那幅DFS示例图。有了颜色有什么用呢?作用大着呢!根据节点的颜色,我们可以对边进行分类!大致可以分为下面四种:

Python算法之图的遍历(python图论算法)

使用DFS对图进行遍历时,对于每条边(u,v),当该边第一次被发现时,根据到达节点 v 的颜色来对边进行分类(正向边和交叉边不做细分):

(1)白色表示该边是一条树边;

(2)灰色表示该边是一条反向边;

(3)黑色表示该边是一条正向边或者交叉边。

下图显示了上面介绍括号定理用时的那个图的深度优先树中的所有边的类型,灰色标记的边是深度优先树的树边

那对边进行分类有什么作用呢?作用多着呢!最常见的作用的是判断一个有向图是否存在环,如果对有向图进行DFS遍历发现了反向边,那么一定存在环,反之没有环。此外,对于无向图,如果对它进行DFS遍历,肯定不会出现正向边或者交叉边。

那对节点标注时间戳有什么用呢?其实,除了可以发现上面提到的那些很重要的性质之外,时间戳对于接下来要介绍的拓扑排序的另一种解法和强连通分量很重要!

我们先看下摘自算法导论的这幅拓扑排序示例图,这是某个教授早上起来后要做的事情,嘿嘿

不难发现,最终得到的拓扑排序刚好是节点的完成时间f[v]降序排列的!结合前面的括号定理以及依赖关系不难理解,如果我们按照节点的f[v]降序排列,我们就得到了我们想要的拓扑排序了!这就是拓扑排序的另一个解法![在算法导论中该解法是主要介绍的解法,而我们前面提到的那个解法是在算法导论的习题中出现的]

基于上面的想法就能够得到下面的实现代码,函数recurse是一个内部函数,这样它就可以访问到G和res等变量

[接下来作者介绍了一个Iterative Deepening Depth-First Search,没看懂,貌似和BFS类似]

如果我们在遍历图时“一层一层”式地遍历,先发现的节点先访问,那么我们就得到了广度优先搜索(BFS)。下面是作者给出的一个有意思的区别BFS和DFS的例子,遍历过程就像我们上网一样,DFS是顺着网页上的链接一个个点下去,当访问完了这个网页时就点击Back回退到上一个网页继续访问。而BFS是先在后台打开当前网页上的所有链接,然后按照打开的顺序一个个访问,访问完了一个网页就把它的窗口关闭。

One way of visualizing BFS and DFS is as browsing the Web. DFS is what you get if you keep following links and then use the Back button once you're done with a page. The backtracking is a bit like an “undo.” BFS is more like opening every link in a new window (or tab) behind those you already have and then closing the windows as you finish with each page.

BFS的代码很好实现,主要是使用队列

Python的list可以很好地充当stack,但是充当queue则性能很差,函数bfs中使用的是collections模块中的deque,即双端队列(double-ended queue),它一般是使用链表来实现的,这个类有extend、append和pop等方法都是作用于队列右端的,而方法extendleft、appendleft和popleft等方法都是作用于队列左端的,它的内部实现是非常高效的。

Internally, the deque is implemented as a doubly linked list of blocks, each of which is an array of individual elements. Although asymptotically equivalent to using a linked list of individual elements, this reduces overhead and makes it more efficient in practice. For example, the expression d[k] would require traversing the first k elements of the deque d if it were a plain list. If each block contains b elements, you would only have to traverse k//b blocks.

最后我们看下强连通分量,前面的分量是不考虑边的方向的,如果我们考虑边的方向,而且得到的最大子图中,任何两个节点都能够沿着边可达,那么这就是一个强连通分量。

下图是算法导论中的示例图,(a)是对图进行DFS遍历带时间戳的结果;(b)是上图的的转置,也就是将上图中所有边的指向反转过来得到的图;(c)是最终得到的强连通分支图,每个节点内部显示了该分支内的节点。

上面的示例图自然不太好明白到底怎么得到的,我们慢慢来分析三幅图 [原书的分析太多了,我被绕晕了+_+,下面是我结合算法导论的分析过程]

先看图(a),每个灰色区域都是一个强连通分支,我们想想,如果强连通分支 X 内部有一条边指向另一个强连通分支 Y,那么强连通分支 Y 内部肯定不存在一条边指向另一个强连通分支 Y,否则它们能够整合在一起形成一个新的更大气的强连通分支!这也就是说强连通分支图肯定是一个有向无环图!我们从图(c)也可以看出来

再看看图(c),强连通分支之间的指向,如果我们定义每个分支内的任何顶点的最晚的完成时间为对应分支的完成时间的话,那么分支abe的完成时间是,分支cd是,分支fg是7,分支h是6,不难发现,分支之间边的指向都是从完成时间大的指向完成时间小的,换句话说,总是由完成时间晚的强连通分支指向完成时间早的强连通分支!

最后再看看图(b),该图是原图的转置,但是得到强连通分支是一样的(强连通分支图是会变的,刚好又是原来分支图的转置),那为什么要将边反转呢?结合前面两个图的分析,既然强连通分支图是有向无环图,而且总是由完成时间晚的强连通分支指向完成时间早的强连通分支,如果我们将边反转,虽然我们得到的强连通分支不变,但是分支之间的指向变了,完成时间晚的就不再指向完成时间早的了!这样的话如果我们对它进行拓扑排序,即按照完成时间的降序再次进行DFS时,我们就能够得到一个个的强连通分支了对不对?因为每次得到的强连通分支都没有办法指向其他分支了,也就是确定了一个强连通分支之后就停止了。[试试画个图得到图(b)的强连通分支图的拓扑排序结果就明白了]

经过上面略微复杂的分析之后我们知道强连通分支算法的流程有下面四步:

1.对原图G运行DFS,得到每个节点的完成时间f[v];

2.得到原图的转置图GT;

3.对GT运行DFS,主循环按照节点的f[v]降序进行访问;

4.输出深度优先森林中的每棵树,也就是一个强连通分支。

根据上面的思路可以得到下面的强连通分支算法实现,其中的函数parse_graph是作者用来方便构造图的函数

[最后作者提到了一点如何进行更加高效的搜索,也就是通过分支限界来实现对搜索树的剪枝,具体使用可以看下这个问题顶点覆盖问题Vertext Cover Problem]

问题5. 强连通分支

In Kosaraju's algorithm, we find starting nodes for the final traversal by descending finish times from an initial DFS, and we perform the traversal in the transposed graph (that is, with all edges reversed). Why couldn't we just use ascending finish times in the original graph&#;

问题就是说,我们干嘛要对转置图按照完成时间降序遍历一次呢?干嘛不直接在原图上按照完成时间升序遍历一次呢?

Try finding a simple example where this would give the wrong answer. (You can do it with a really small graph.)

总结

标签: python图论算法

本文链接地址:https://www.jiuchutong.com/biancheng/372277.html 转载请保留说明!

上一篇:Python之Scrapy爬虫框架安装及使用详解(python scrapy爬虫)

下一篇:详解python eval函数的妙用(eval()函数python)

  • 流转税包括哪些税种2022
  • 行政单位要不要税号
  • 进口货物如何确认
  • 报送会计报表
  • 经营范围没广告怎么办
  • 未达起征点销售额和小微企业免税销售额
  • 宣传费属于什么税目
  • 去税务局申报需要带营业执照吗
  • 抵扣联的抵扣期限
  • 公司支付保险公司保费怎么做账
  • 公司厂房出租发票怎么开
  • 没有计提坏账准备的应收帐款坏帐帐务处理
  • 增值税普票只要发票号吗
  • 金税三期个人客户端在哪下载
  • 增值税留抵税额账务处理
  • 技术转让免征增值税文件
  • 残疾人就业保障金是什么意思啊
  • 忘记excel工作表保护密码怎么办
  • 财务差旅费报销制度
  • php 反射
  • php如何通过链接获取源码
  • 苹果 macOS 13.3 开发者预览版 Beta 2 发布
  • 补缴社保操作流程
  • regsync.exe - regsync是什么进程 有什么用
  • PHP:spl_autoload_unregister()的用法_spl函数
  • 跨期摊提类账户
  • php 堆排序
  • 新注册的外贸公司花名册
  • 生产企业出口货物增值税如何申报
  • 收到员工罚款分录
  • 海峡群岛属于哪个洲
  • 投资者追加资本金属于什么
  • ssm算前后端分离吗
  • php数组的概念是什么
  • php如何生成html
  • vuecli非根目录打包
  • php sse
  • 既简单又安全的小实验
  • vue的安装步骤
  • php图像识别技术是什么
  • php构造函数重载
  • 研发费用加计扣除75%还是100%
  • 公司的银行账号是不是和个人账号不一样
  • 商誉 减值
  • 新公司不开户需要交税吗
  • 事业单位购入固定资产当月计提折旧
  • 一般纳税人年收入500万交多少税
  • 玉米 收购
  • 实收资本可以用于偿还借款
  • 会计信息采集每年都要采集吗
  • 控股合并和吸收合并会计处理的区别
  • 建筑企业包工包料业务的发票开具和涉税处理
  • 专项补助资金的账务处理
  • 虚增利润怎么调整
  • 可以自行开具增值税专用发票的行业有哪些
  • 建设工程施工管理
  • 公务机票保险费能报销吗
  • 暂估成本对冲分录怎么写
  • 低值易耗品五五摊销法报废
  • mysql mod
  • mysql如何跨库查询
  • mysql的慢查询日志怎么查看
  • gentoo安装教程2021
  • win71
  • 如何使用xp
  • Linux系统安全管理的内容包括
  • 关闭windows报错
  • win7系统宽带连接651
  • forfiles命令详解
  • bat批处理执行cmd命令
  • 用bat调用exe并输入参数
  • 获取控件的值
  • websocket方法
  • javascript基础教程pdf下载
  • 'd:skin' 开头的无效内容。此处不应含有子元素。
  • JavaScript fontsize方法入门实例(按照指定的尺寸来显示字符串)
  • jquery数据类型
  • jquery-validate
  • python的入门教程
  • weverse登录不了
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设