强连通分量
强连通分量只可能存在于有向图中,无向图中是不存在强连通分量的。下图很形象地指出了图中所包含的强连通分量。
上图中存在5个强连通分量(阴影部分)。
拓扑排序
定义:给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素。
如下图所示,左边是一个有向无环图,箭头方向代表了其执行方向,也就是边的方向。右边是通过计算得到的拓扑排序,其实从图中可以看出,拓扑排序最重要的就是为我们的有向图理顺了其内在的先后顺序,应用中一般使用拓扑排序来解决调度问题。
划重点:一幅有向无环图的拓扑顺序即为所有顶点的逆后序排列。
Kosaraju算法
算法流程:
(1)对原图G取反GT(也称转置图)
(2)从任意一个顶点开始对反向图GT进行逆后续DFS遍历,获得图的拓扑排序reversePost
(3)按照逆后续遍历reversePost中 节点的出栈顺序,对原图G 进行DFS遍历,一次DFS遍历中访问的所有顶点都属于同一强连通分量。
算法思路:
图G 中有两个强连通分量,即强连通分量A 和强连通分量B,分别由顶点 A0-A1-A2 和顶点 B3-B4-B5 构成。每个连通分量中有若干个可以相互访问的顶点(这里都是3个),强连通分量与强连通分量之间不会形成环,否则应该将这些连通分量看成一个整体,即看成同一个强连通分量。
我们假设,DFS从强连通分量B 的任意一个顶点开始,那么恰好遍历整个图需要2次DFS,而且每次DFS遍历的顶点恰好属于同一个连通分量。我们若从连通分量A 中任意一个顶点开始DFS,此时我们只需要一次DFS就访问了所有的顶点。所以,我们不应该按照顶点编号的自然顺序或者任意其它顺序进行DFS,而是应该按照被指向的强连通分量的顶点排在前面的顺序进行DFS。上图中由强连通分量A指向了强连通分量B。所以,我们按照
B3, B4, B5, A0, A1, A2
的顺序进行DFS,这样就可以达到我们的目的。但事实上这样的顺序太过严格,我们只需要保证被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面即可,比如
B3, A0, A1, A2, B4, B5
B3排在了强连通分量A所有顶点的前面。
这就是Kosaraju的算法思想:对原图取反,然后从反向图的任意节点开始进行DFS的逆后序遍历,逆后序得到的顺序一定满足我们的要求。
DFS的逆后序遍历是指:如果当前顶点未访问,先遍历完与当前顶点相连的且未被访问的所有其它顶点,然后将当前顶点加入栈中,最后栈中从栈顶到栈底的顺序就是我们需要的顶点顺序。
上图表示原图G 的反向图GT。
我们现在进行第一种假设:假设DFS从位于强连通分量A 中的任意一个节点开始。那么第一次DFS完成后,栈中全部都是强连通分量A的顶点,第二次DFS完成后,栈顶一定是强连通分量B的顶点。保证了从栈顶到栈底的排序强连通分量B的顶点全部都在强连通分量A顶点之前。
我们现在进行第二种假设:假设DFS从位于强连通分量B 中的任意一个顶点开始。显然我们只需要进行一次DFS就可以遍历整个图,由于是逆后续遍历,那么起始顶点一定最后完成,所以栈顶的顶点一定是强连通分量B中的顶点,这显然是我们希望得到的顶点排序的结果。
上面使用了最简单的例子说明Kosaraju算法的原理,综上可得,不论从哪个顶点开始,图中有多少个强连通分量,逆后续遍历的栈中顶点的顺序一定会保证:被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面。
以下摘自算法第四版中的原文:
下图4.2.16所示为Kosaraju算法处理一幅有向无环图(右上角的图)时的轨迹。在每次dfs()调用轨迹的右侧都是有向图的一种画法,顶点按照搜索结束的顺序排列。因此,从下往上来看左侧这幅有向图的反向图得到的就是所有顶点的逆后序。也就是在原始的有向图中进行深度优先搜索时所有未被标记的顶点被检查的顺序。你可以从图中看到,在第二遍深度优先搜索中,首先调用的是dfs(1)(标记顶点1),然后调用的是dfs(0)(标记顶点0、5、4、3和2),然后检查了顶点2、4、5和3,再调用dfs(11)(标记顶点11、12、9和10),在检查了9、12和10之后调用dfs(6)(标记顶点6),最后调用dfs(7)标记了顶点7和8。最终得到了连通分量数量为5。