注:本算法和计算图所有结点对最短路径的Johnson算法不同。


目录

综述

代码解析

实例解析

引用


综述

Johnson算法由B. Johnson发表于1975年,用于在一个有向图中寻找所有简单环。时间复杂度上界为O((n+e)(c+1)),空间复杂度为O(n+e),其中n为顶点数,e为边数,c为存在环数。在算法执行时,每两个连续的环的输出之间的时间不会超过O(n+e)。

*常用的此类算法的复杂度比较如下:

  1. Tiernan - O(V.const^V)
  2. Tarjan - O(VEC)
  3. Johnson - O(((V+E)C)
  4. Szwarcfiter and Lauer - O(V+EC)

所谓的简单环,即除了第一个和最后一个顶点,其余所有顶点在路径中只出现一次。这里排除了包括像v->v这样的自循环的边和在两个顶点之间的多条边的情况。本算法沿用Tiernan算法的记号,每个简单环都是由根顶点为s的子图构建的,这个子图由s和“大于”s的顶点构成。因此每个输出都根据路径的最小点s来分类。

当从s开始遍历路径,把一个顶点v加入这个路径时,会把这个顶点的状态设为“阻塞”(blocked),直到从v到s的所有路径都完成检索,v会一直保持这种状态,这样保证了一个顶点不会被重复添加。另外除非一个顶点是构造简单环路径的最小的点,这个点不会成为路径的根顶点。这样保证不会无意义的搜索。

算法的输入是一个图结构,图的表示是邻接表形式。假设每个顶点用从1到n的一个数字表示,图表示为AG,则AG(v)是一个数组表示第v个顶点的邻接顶点。算法的过程是这样的:从一个根顶点s开始构建简单环,所经过的路径的顶点保存在一个栈中。算法通过调用CIRCUIT过程添加新顶点,添加时务必将其设置为blocked,并且在过程调用返回时删除这个顶点,但是此时不一定将其阻塞状态去除。

代码解析

下面是算法的伪代码:

如上所述,整个算法包括CIRCUIT主过程, 位于empty stack;语句以上,它又包含一个子过程UNBLOCK用于对一个blocked顶点进行解锁。下面以C++为例谈一下我对这个算法实现的理解:

每个点的ID表示为1到n的一个类型为size_t整数。Ak,B两个数组在开始时被初始化,数组的长度都为n,即顶点数目。这两个数组的元素都是list<size_t>,A数组第i个元素的链表记录从点i出发指向的顶点,即图的邻接表。B的作用是记录因为此点阻塞而被迫阻塞的顶点,一旦点i因为调用unblock过程解锁,那么在B[i]链表中记录的顶点也因此解锁。B的效果是将每个点尽可能的保持阻塞状态以防止重复搜索。blocked是一个长度为n的bool数组,表示在搜索时这个点是否是阻塞。s表示目前搜索到的根结点的编号。

CIRCUIT过程从stack v;语句开始,其输入参数v表示这个CIRCUIT过程所搜索的环都是从v这个顶点开始的。调用开始先将标志量f设为false,f表示这次调用是否发现了一个环。把v放在栈stack中,然后将blocked数组的v设为true,即将此点阻塞。可以看到下方对于CIRCUIT的调用都是将s作为参数,所以stack的栈底一般都是s。下一步遍历Ak中第v个链表,即v出发的边指向的顶点。如果某个顶点与s相等,说明找到了一个环,这时执行输出环并把f设为true。否则如果这个顶点没有阻塞,说明还可以向下走,就递归调用此过程。

如果能够在v的邻接顶点中找到一条环路,则f就是true的,这时可以把v解锁,即调用unblock过程。反之,则说明从包含从v出发的边的路径都是死路,是不可能的形成环的,所以不能解锁v,同时要把与v邻接的顶点置于B数组的第v个链表中,经过v的路径是死路,但经过v的邻接顶点w的不一定死路,应为w可能有另外的上级顶点,所以如果不搜索v了,即解锁v顶点是要把B[v]中记录的顶点也解锁,以供其他路径搜索使用。

对v的调用结束时把v出栈。返回f,标志在这次调用中是否发现环。

下面是如何执行主进程。主进程涉及到寻找强连通分量(scc),这部分可以参考tarjan算法。算法首先清空储存路径节点的栈,将搜寻起点s设为1,注意所有顶点编号从1开始。Ak初始化为在{s, s+1, s+2,...,n}这些顶点构成的子图里面所有强连通分量中,有最小顶点的分量的邻接表。Ak如果为空,说明图已经搜索完,算法结束。否则将s设为上述分量Ak里面最小的顶点,将blocked全置为false,B清空,以s为输入参数调用CIRCUIT。注意此时CIRCUIT只寻找以s开头的环路。调用完成将s加一,再在新的子图中寻找。

实例解析

下面是对上述过程的一个实例,来自YouTube一位Tushar Roy的视频主的教程(此君在YouTube上多有图论方面教程,可以学习,这个算法的视频我搬运到了B站,供诸君参考)。

Step1是一个有向图G,Step2表示分解后的三个强连通分量,所谓强连通分量,就是在一个有向图中,任选两个顶点都可以互相到达。分解scc的tarjan算法在此不再赘述。

发现1是最小的顶点,选取1所在的scc开始搜索,第一个调用顶点是1。step3-5是从1开始的3条遍历路径。上图所示,红色边表示搜索的路径,从1开始依次搜索1,2,3,到3的时候,栈里存储了这三个节点,并且blocked数组将这三个元素设为true。3有3个邻接顶点。首先搜索1,发现1是出发的顶点,表明找到了一个环,就将环输出,并把3这里调用的变量f设为true。

回到顶点3后搜索下一个邻接顶点4,4没有阻塞说明还没有探索过,4只有一个邻接点5,走到5之后已经无路可走,因为5的唯一邻接顶点2处于blocked状态,所以以f为false返回这次调用,表明没有找到环。调用从2返回到5的时候看到返回值是false,这时把5放到B[2]的链表中。这种做法相当于告诉2节点:兄弟你解锁的时候把我也解锁了,你不解锁我也堵着算了,反正下次再找到我的时候,我的下家都是死路,找也是白找。所以,只有在调用UNBLOCK(2)的时候5才能解锁,这时blocked[5]还是保持blocked状态。返回的4的时候也是如此。需要把4保持阻塞,并把4放到B[5]的链表中。这时调用返回到了3节点。

检索3的下一个邻居6,发现6的下家4还是阻塞的,于是这次调用也以失败返回。返回到3,3的邻居已经检索完,发现了一个环,所以f是true的,把3的阻塞解除,调用返回到2。因为3找到了一个环所以2的f也是true的,所以调用UNBLOCK(2),这个过程把blocked[2]设为false之后,检索B[2]发现里面有一个元素5,因此递归调用把5也解锁了。依次调用,B就被清空了。

这就是一次1->2这个边开头的检索的完整过程。

本人水平有限,解读如果有失误之处欢迎交流。

引用

[1] Donald B. Johnson, Finding all the elementary circuits of a directed graph, SIAM Journal on Computing, 1975.

[2] BiliBili搬运视频:https://www.bilibili.com/video/BV13Q4y1K7bx

[3] 一个Github的C++实现:https://github.com/hellogcc/circuit-finding-algorithm(其实现无论方式还是算法都有不少和原算法的出入,大家批判性鉴赏。

在有向图中找出所有简单环--Johnson算法相关推荐

  1. 算法期中1007. 怪兽训练 (找出有向图中所有的强连通分量的Kosaraju算法)

    Description 贝爷的人生乐趣之一就是约战马会长. 他知道马会长喜欢和怪兽对决,于是他训练了N只怪兽,并对怪兽用0到N-1的整数进行编号. 贝爷训练怪兽的方式是让它们一对一互殴. 两只怪兽互殴 ...

  2. 【面试现场】如何在10亿数中找出前1000大的数

    小史是一个应届生,虽然学的是电子专业,但是自己业余时间看了很多互联网与编程方面的书,一心想进BAT互联网公司. 之前小史在BAT三家的面试中已经挂了两家,今天小史去了BAT中的最后一家面试了. 简单的 ...

  3. 从一个数组中找出 N 个数,其和为 M 的所有可能--最 nice 的解法

    比起讨论已经存在的大牛,我们更希望有更多有潜力的前端小伙伴成为大牛,只有这样,前端在未来才能够持续不断的发光发热. 故事的背景 这是一个呆萌炫酷吊炸天的前端算法题,曾经乃至现在也是叱咤风云在各个面试场 ...

  4. 改进,从一个数组中找出 N 个数,其和为 M 的所有可能

    特此说明,本文算法改自于<从一个数组中找出 N 个数,其和为 M 的所有可能--最 nice 的解法>一文.本文不同的是,采用二进制正序表示法,这种实现思路更直观.更简单些. 问题 从一个 ...

  5. Redis进阶-如何从海量的 key 中找出特定的key列表 Scan详解

    文章目录 需求 scan scan基本使用 批量写入一批模拟数据 字典的结构 scan 遍历顺序 (高位进位法) 渐进式 rehash 更多的 scan 指令 大 key 扫描 --bigkeys 使 ...

  6. 海量数据处理 - 10亿个数中找出最大的10000个数(top K)

    海量数据处理 - 10亿个数中找出最大的10000个数(top K问题) 版权声明:本文为博主原创文章,未经博主允许不得转载 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望 ...

  7. linux误修改文件名恢复,如何在 Linux 中找出最近或今天被修改的文件-linux修改文件名...

    Linux 用户在命令行上遇到的常见问题之一是定位具有特定名称的文件,如果你知道确定的文件名则可能会容易得假设你忘记了白天早些时候创建的文件的名称(在你包含了数百个文件的 home 文件夹中),但现在 ...

  8. c++如何输入数组_从一个数组中找出 N 个数,其和为 M 的所有可能最 nice 的解法...

    编者按:本文由前端狂想录公众号授权奇舞周刊转载. 故事的背景 这是一个呆萌炫酷吊炸天的前端算法题,曾经乃至现在也是叱咤风云在各个面试场景中. 可以这样说,有 90% 以上的前端工程师不会做这个题目. ...

  9. 数据结构与算法--有序数组中找出和为s的两个数字

    有序数组中找和为s的两个数字 题目:输入一个递增排序的数组array, 和一个数字s, 在数组中找出两个数,使得这两个数的和是s,如果有多对,输出一对即可. 最简单方案 双循环,每次获取一个数据,和数 ...

  10. 10亿个数中找出最大的10000个数

    转载自  海量数据处理 - 10亿个数中找出最大的10000个数(top K问题) 前两天面试3面学长问我的这个问题(想说TEG的3个面试学长都是好和蔼,希望能完成最后一面,各方面原因造成我无比想去鹅 ...

最新文章

  1. 压缩版styleGAN
  2. 边工作边刷题:70天一遍leetcode: day 94-1
  3. vim paste indent problem
  4. 【pmcaff】社交媒体时代,村姑如何找到真爱?
  5. 我对对象和引用的理解
  6. ai二维码插件_超实用的AI脚本插件合集2.0免费分享,让你的设计快人一步
  7. linuxpython源文件_Python3 源码安装(Linux 版)
  8. android BaseAdapter优化
  9. 【技术框架汇总】_开发平台_前端框架_手机端框架_测试工具_数据库中间件_监控工具_框架_汇总
  10. 有限元微分方程求解方法,能量原理,瑞利里兹法,伽辽金法(曾攀有限元分析)
  11. 线性代数知识点(行列式篇)
  12. 一定要看:如何成为一名全栈工程师?
  13. 使用Windows Server 2003实现高可用故障转移群集(1)
  14. 实现数字电视机顶盒画面的纯键盘和遥控操作网页
  15. ae如何把已有图片当做蒙版_AE遮罩教程,如何用AE创建文字蒙版遮罩
  16. 【数据分析与挖掘实战】金融风控之贷款违约预测详解2(有代码和数据集)
  17. 白平衡之灰度世界算法
  18. tf.constant(常量)
  19. 显示行数 设置ssh终端_mac下终端iTerm2配置
  20. ROS机器人项目开发11例-ROS Robotics Projects(10)机器人Web工具集

热门文章

  1. 纯js读取excel文件内容,支持所有刘浏览器
  2. PCL RANSAC点云配准
  3. 音频噪声抑制_音频编辑入门指南:基本噪声消除
  4. Mac install ninja_玩游戏出现机器码或被封,用修改网卡mac物理地址的方法试一试...
  5. Win1909+vs2019+Windows 10 WDK 2004(10.0.19041.1) + Windows 10 SDK 2004(10.0.19041.1)环境搭建
  6. android之StorageManager介绍
  7. 干货:怎么提高科技成果转移转化成效?
  8. 瑞利信道下BPSK的误码率
  9. 大话数据结构-单链表勘误,计划调整
  10. tftp服务器怎么开启linux,启动Linux下的TFTP服务器