【概述】
Tarjan 算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。
搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
【基本思路】
定义 DFN(u) 为节点 u 搜索的次序编号(时间戳),即是第几个被搜索到的,Low(u) 为 u 或 u 的子树能够追溯到的最早的栈中节点的次序号。
每次找到一个新点 i,有:DFN(i)=low(i)
当点 u 与点 v 相连时,如果此时(时间为 DFN[u] 时)v不在栈中,u 的 low 值为两点的 low 值中较小的一个
即:low[u]=min(low[u],low[v])
当点 u 与点 v 相连时,如果此时(时间为 DFN[u] 时)v 在栈中,u 的 low 值为 u 的 low 值和 v 的 dfn 值中较小的一个
即:low[u]=min(low[u],dfn[v])
当 DFN(u)=Low(u) 时,以 u 为根的搜索子树上所有节点是一个强连通分量。
【流程】
以下图为例,共有三个强连通分量:1234、5、6
从节点 1 开始 DFS,把遍历到的节点加入栈中,搜索到节点 u=6 时,DFN[6]=LOW[6]=4,找到了一个强连通分量 {6}
返回节点 5,发现 DFN[5]=LOW[5]=3,退栈后 {5} 为一个强连通分量。
返回节点 3,继续搜索到节点 4,把 4 加入堆栈。发现节点 4 像节点 1 的后向边,节点 1 还在栈中,所以 LOW[4]=1。节点 6 已经出栈,不再访问 6,返回 3,(3,4) 为树枝边,所以 LOW[3]=LOW[4]=1。
继续回到节点 1,最后访问节点 2。访问边 (2,4),4 还在栈中,所以 LOW[2]=4。返回 1 后,发现 DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量 {1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2}、{5}、{6}。
【时间复杂度】
通过上述流程分析,运行 Tarjan 算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为 O(N+M)。
【实现】(非转载)
#include <stack>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define M 10005
vector<int>G[M];
stack<int>ss;
int n, m, u, v, time, ans;
int Low[M], DFN[M], c[M];
//c[i]表示i点所属的强联通系
bool vis[M], flag;
inline void dfs(int x){
vis[x] = 1;
int son;
time++;
Low[x] = DFN[x] = time;
ss.push(x);
int siz = G[x].size();
for(int i = 0; i < siz; i ++){
son = G[x][i];
if( !DFN[son] ){
dfs(son);
Low[x] = min(Low[x], Low[son]);
}
else if( vis[son] )
Low[x] = min(Low[x], DFN[son]);
}
if( Low[x] == DFN[x] ){
ans ++;
while(1){
int t = ss.top();
ss.pop();
c[t] = ans;
if( t == x )
break;
}
}
}
int main(){
while( scanf("%d%d", &n, &m) != EOF ){
if( !n && !m )
return 0;
flag = time = ans = 0;
memset(vis, 0, sizeof(vis));
memset(DFN, 0, sizeof(DFN));
while( !ss.empty() )
ss.pop();
memset(G, 0, sizeof(G));
for(int i = 1; i <= m; i ++ ){
scanf("%d%d", &u, &v);
G[u].push_back(v);
}
for(int i = 1; i <= n; i ++){
if( !vis[i] )
dfs(i);
}
printf("%d\n", ans);
}
return 0;
}