比较硬核的开头:
最近因为要用到一些图拓扑排序的问题,因此复习了一些图算法,并用python实现了一下。这里先放出全部代码(包括带注释的py文件和可以运行的jupyter文件)的下载参考地址(封面图是repo中用到的例子):https://github.com/zhouyanasd/Topological_sorting_tarjan
比较硬核的原理解释:
目录
- 深度优先搜索 (Depth-first search, DFS)
- 强联通分量(Strongly connected components)
- 拓扑排序(Topological sorting)
1. 深度优先搜索 (DFS)
深度优先搜索是数据结构的图算法中相当基础的部分,核心思想是从根节点开始(可以从图中任意选择,但是不同的根节点搜索后得到的子树不一样),递归对它连接的后续节点进行访问。先拿出《算法导论》中的伪代码和例子。
伪代码:
例子:
结合伪代码和例子,DFS首先初始化所有节点为白色(未被访问过),选择u为根节点开始访问,v为下一个被访问的子节点。当节点访问时,标记为灰色(正在被访问),当子节点为白色时则递归地对其进行访问。当没子节点可以访问时,则层层退出递归,也叫做回溯,如图(f),即回溯到某个灰色节点时,将其状态变为黑色(完成访问)。算法会维护一个访问step,节点变灰时记录其发现时间,当变成黑色时记录完成时间。如图(g)的y节点的发现时间是3,完成时间是6。
2. 强联通分量(Strongly connected components)
首先看一个什么是强连通分量:
有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
换句话说强连通分量就是有向图中的环,所有可以成环的点,只要能接触到其中一个,就可以通过链接到达任意一个。
那么从上面的例子可以看到,这里的强联通分量是(u,x,v,y),(w)和(z)三个。
但是我们要用搜索的办法来找到他们,主要的方法有3个:
- Kosaraju算法
- Tarjan算法
- Gabow算法
Kosaraju算法:
这个比较好理解,就是在第一次DFS之后,将图的方向反转,从最晚finish的节点开始再进行一次DFS。具体这里不做解释了,网上和算导中有很多帮助理解的资料。
Tarjan算法和Gabow算法:
这两个算法是对Kosaraju的一个改进,仅用了一次DFS即可找到强连通分量。
这里重点介绍一下Tarjan算法:
算法基本上是在DFS上进行了改进,首先算法引入了一个堆栈stack,和一个数组low。stack用来存储访问过的节点,low用来记录当前节点所属连通分量的根。
算法伪代码:
以上面的图为例,从u开始直到x(图e),算法中状态数组和stack如下:
这时发现x的子节点u已经被访问,这时需要比较x的low和u的discorvary,取小值即low(u)=discorvary(u)=1。那么自此开始回溯,每当回溯到一个节点时,比较当前节点和后续节点的low值,取小值。因此不难判断,节点x、y、v的low都变成和x一样为1。这时的状态数组为:
这时我们来到图j,由于伪代码中需要判断当low和discorvary相同时才可以完成搜索,所以这里和DFS的区别在于回溯后并没有立刻结束节点搜索,而节点仍然存在stack中。当回溯到u时,low(u)==discorvary(u),因此开始进行出栈。在栈顶是x!=u,所以继续出栈,直到u出栈后,将这些出栈的节点作为一个强连通分量进行记录。
接下来继续遍历w,过程基本上和之前相似,这里省略啦。
Gabow算法
Gabow是Tarjan算法的改进版,只是它用第二个栈来代替Tarjan算法里面的low和u的discorvary数组。具体这里暂时没有实现,不多做介绍啦。
3. 拓扑排序(Topological sorting)
终于来到重点了,前面做了这么多,其实就是为了进行排序。我们都知道,拓扑排序是只有在有向无环图(DAG)中才能进行,但是有环图是没办法直接进行拓扑排序的。
如何解决呢?可以先将图中的环(即强连通分量)找出,进行缩点(即将强联通分量看做为一个点),这样就可以保证缩点后的图中不存在环,也就可以进行拓扑排序了。
对于DAG,拓扑排序的顺序即为finish时刻的倒序,对于有环图来说,强联通分量组成的”大节点“中的finish一般是连续的,因此可以任意找出一个点来代表(可以是搜索中作根的节点)来进行排序。
另外,如果记录强连通分量的数据结构是有顺序的话,可以发现,发现强联通分量的顺序即为拓扑排序的倒序(这里将单独的一个节点也看做一个分量)。
硬核的总结:
算法的解析基本上到这里了,具体的细节可以直接到code中体会,还可以查一些其他的资料,这里不赘述了(懒,不想多写了)。