有向图的连通性
有向图的连通性
一、基本概念
强连通:在有向图 $G$ 中,如果两点 $u$ , $v$ 是互相可达的,则称 $u$ 和 $v$ 是强连通的。如果 $G$ 中的任意两个点都是互相可达的,那么 $G$ 就是强连通图。
强连通分量:如果一个有向图 $G$ 不是强连通图,那么可以把它分成多个子图,其中每个子图的内部是强连通的,而且这些子图已经扩展到最大,不能与子图外的任一点强连通,像这样的一个“极大强连通”子图是$G$的一个强连通分量( $\text{Strongly Connected Component}$,$\text{SCC}$ ).
$Tarjan$算法能在一次DFS中吧所有点都按 $\text{SCC}$ 分开。这并不是不可思议的,它利用了 $\text{SCC}$ 的特点。
定理:一个 $\text{SCC}$,从其中任何一个点出发。都至少有一条路径能绕回到自己。
二、算法实现
在讲解之前,先了解$low$和$num$操作。
下面是一个例子,下图有三个 $\text{SCC}$,也就是$\{a,b,d,c\}$、$\{e\}$、$\{f\}$.
图1.a是原图。图1.b是对它做$DFS$,每个点左边的数字标记了$DFS$访问它的顺序,也就是$num[]$值,右边的划线数字是$low[]$值,即能返回到的最远的祖先。每个点的$low[]$初始值等于$num[]$,即连到自己。观察$c$的$low[]$值是如何更新的:它的初始值是$6$,然后有一个回退到$a$,所以更新为$1$;它的递归祖先$d$、$b$的$low[]$值也跟着更新为$1$。$e$和$f$的$low[]$值不能更新。
图1.b是从$a$开始$DFS$的,$a$成为{$a,b,d,c$}这个$SCC$的祖先.其实,从{$a,b,d,c$}中任意一个点开始$DFS$,这个点都会成为这个$SCC$的祖先。认识到这些,可以帮助我们理解后面的解释:可以用栈分离不同的$SCC$。
图1.b中的$low[]$值有$3$部分,即等于$1$的{$a,b,d,c$}、等于$4$的{$f$}、等于$5$的{$e$}。这就是$3$个$SCC$。
完成以上步骤,似乎已经就解决了问题。每个点都有了自己的$low[]$值,相同$low[]$值的点属于一个$SCC$。那么只要再对所有点做一个查询,按$low[]$值分开就行了,其复杂度是$O(V)$。
其实有更好的办法,即在$DFS$的同时把点按$SCC$(有相同的$low[]$值)分开。
以图2为例,其中有3个$SCC$,即$A$、$E$、$F$。假设从F中的一个点开始$DFS$,$DFS$过程可能会中途跳出$F$,转入$A$或者$E$,总之,最后会进入一个$SCC$。
假设DFS过程是$F$->$E$->$A$,最后进入$A$。
在$A$这个$SCC$中将完成$A$内所有点的$DFS$过程,也就是说,最后的几步$DFS$会集中在A中的点$a$、$b$、$c$、$d$。这几个点会计算得到相同的$low[]$值,标记为一个$SCC$,这样就好了。
$DFS$递归从$A$回到$E$。并在E中完成$E$内部的$DFS$过程。
回到$F$,在$F$内完成递归过程。
以上过程如何编程?那你可能想起来,$DFS$搜索是用递归实现的,而递归和栈这种数据结构在本质上是一致的。所以,可以用栈来帮助处理。
从$F$开始递归搜索,访问到的某些点进入栈;
$E$中的某些点进入栈;
在$DFS$的最底层,A的所有点将被访问到并进入栈,当前栈顶的几个元素就是$A$的点,标记为同一个$SCC$,并弹出栈;
$DFS$回到$E$,在$E$中完成所有点的搜索并且入栈,当前栈顶的元素就是$F$的点,标记为同一个$SCC$,并弹出栈;
回到$F$,完成$F$的所有点的搜索并且入栈,当前栈顶的几个元素就是$F$的点,标记为同一个$SCC$,并弹出栈、结束。
为加深对上述过程中栈的理解,我们可以思考最先进入栈的点。每进入一个新的$SCC$,访问并进入栈的第一个点都是这个$SCC$的祖先,它的$num[]$值等于$low[]$值,这个$SCC$中所有点的$low[]$值都等于它。
三、样例练习
一道模板题。代码
hdu 1827 Summer Holiday: Tarjan 缩点模板题。
hdu 3072 Intelligence System: Tarjan+贪心。
hdu 3639 Hawk-and-Chicken: 强连通分量+缩点。
hdu 3861 The King’s Problem: 最小路径覆盖。
hdu 1530 Maximum Clique: 最大团简单题目。