在一次BFS或DFS中,我们其实并不能保证一定访问到图中的所有节点,因为有些图可能是不连通的。我们把从一个点出发,所有可达点的集合称为这个点所在的连通分量。给定一个无向图,我们找所有连通分量的方法叫做灌水法(Flood Fill),其实就是对当前未访问过的点做BFS/DFS,直到所有的点都被访问过1次。
Tarjan算法是为了解决有向图中类似的问题提出的。只不过有向图中我们可以定义强连通分量,有向图中一个强连通分量中的任意两个点u,v都是强连通的,即存在从u到v的路径,也存在从v到u的路径。
很明显,Flood Fill并不能用来求强连通分量。但只使用BFS/DFS,我们可以给出一个求给定点所在强连通分量的方法:1) 从该点出发做一次BFS/DFS;2) 把所有边反向,再从这个点做一次BFS/DFS;3) 把两次搜索访问的顶点集合做一次交,就可以得到该点所在的极大强连通分量。如果用这种方法求所有强连通分量的话,需要对每个点做两次BFS/DFS。更好的方法是Kosaraju算法或Tarjan算法,这里只介绍Tarjan,它只需要对图做一遍DFS。
在进入Tarjan算法之前,你需要明白对图做DFS可以得到一棵树,树的根节点是DFS的起点,每次从点u访问一个新的点v,就引一条从u到v的边。
Tarjan算法中,图中每个节点维护两个属性:
- dfu(i):节点i在DFS中第dfu(i)个被访问到(时间戳);
- low(i):DFS搜索树中,以节点i为根节点的子树中的节点集合记为T(i)。T(i)中的点在原图中所指向的点的集合记为S(i)。S(i)中最小的时间戳就是low(i)。
Tarjan算法的描述如下:
- 初始化一个空的栈和一个每访问一个节点加1的计时器
- 对图做DFS,每次访问新的顶点时,设定dfn(i)为当前时间,把该节点压栈,接着求low(i):对于还没被访问的后继节点j,递归访问j,
low(i) = min(low(i), low(j))
,对于已经访问过的且在栈中的后继节点j,low(i) = min(low(i), dfn(j))
。如果最终得到的low(i) = dfn(i)
,就把栈中当前节点以上的节点全部弹出,这些节点就是一个极大强连通分量
Tarjan算法只需要做一遍DFS,所以一定会终止,时间复杂度O(m+n)。
C++实现:
void tarjan(int u) {
dfn[u] = low[u] = ++cnt;
st[++top] = u;
instack[u] = true;
for(Edge *p = e[u]; p; p = p->next) {
int v = p->dst;
if(!dfn[v]) {
tarjan(v);
low[u] = std::min(low[u], low[v]);
}
else if(instack[v])
low[u] = std::min(low[u], dfn[v]);
}
if(dfn[u] == low[u]) {
cluster++;
int hd;
do {
hd = st[top--];
instack[hd] = false;
belong[hd] = cluster;
}
while(hd != u);
}
}
(PS:一道经典的求强连通分量的题 Networks of School
网友评论