首先解释一下三个概念
强连通(strongly connected): 在一个有向图G里,设两个点 a b 发现,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
强连通图: 如果 在一个有向图G中,每两个点都强连通,我们就叫这个图,强连通图。
强连通分量strongly connected components):在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量
我们先来看一段简要的伪代码
tarjan(u){
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点u还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
再来一张网上很多人用过的图来做解释
从1进入 DFN[1]=LOW[1]= ++index ----1
入栈 1
由1进入2 DFN[2]=LOW[2]= ++index ----2
入栈 1 2
之后由2进入3 DFN[3]=LOW[3]= ++index ----3
入栈 1 2 3
之后由3进入 6 DFN[6]=LOW[6]=++index ----4
入栈 1 2 3 6
到达6之后发现6没有出度,递归完毕,同时发现DFN【6】 == LOW【6】,出栈,然后返回上一级,上一级中更新LOW【3】 = min(LOW【3】, LOW【6】),发现没有变,然后判断出DFN【3】 == LOW【3】,出栈,返回上一级,然后在结点2发现有出度,走结点5这一条路,结点5有俩条出度,6已经被访问过了,不再访问,1也被访问过了,但是1还在栈中,于是我们更新LOW【5】 = min(LOW[5】, LOW【1】) = 1,这时候判断LOW【5】 != DFN【5】,所以5不用出栈,直接递归完毕返回上一级结点2,结点2更新LOW【2】 = min(LOW[2], LOW[5]) = 1,同样LOW【2】 != DFN【2】, 2也不用出栈,返回上一级结点1,1有一条没有访问的出度,所以我们访问4,然后在节点4发现5已经访问过但是还在栈中,于是LOW[4] = min(LOW[4], DFN[5]) = 5,判断出DFN[4] != LOW【4】,所以不用出栈,然后返回上一级结点1,更新LOW【1】 = min(LOW[1], LOW[4]), 没有变化,但是发现DFN[1] == LOW[1],然后栈里面1以及1以上的结点全部出栈,tarjan算法模拟完毕
贴一段targin算法,算法的输入以及输出就是上面模拟的例子
input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
output:
6
5
3 4 2 1
#include<iostream>
using namespace std;
const int maxn = 10010;
int head[maxn], DFN[maxn], LOW[maxn], Stack[maxn], vis[maxn];
int cnt = 1, n, m, a, b;
//链式前向星,不知道的可以百度去学一下
struct Node{
int to;
int w;
int next;
}edge[2 * maxn];
void add(int u, int v, int w) {
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];
head[u] = cnt++;
}
int total, index;
void tarjan(int u) {//代表第几个点在处理。递归的是点。
DFN[u] = LOW[u] = ++total;// 新进点的初始化。
Stack[++index] = u;//入栈
vis[u] = 1;//表示在栈里面
for (int i = head[u]; i != 0; i = edge[i].next) {
int v = edge[i].to;
if (!DFN[v]) {//如果没有访问过
tarjan(v);//递归访问
LOW[u] = min(LOW[u], LOW[v]);//找最小父根
} else if (vis[v]) {//如果访问过,并且还在栈里面
LOW[u] = min(LOW[u], DFN[v]);//找最小父根
}
}
if (DFN[u] == LOW[u]) {//发现是父根
do{
cout << Stack[index] << " ";
vis[Stack[index]] = 0;
index--;
} while (Stack[index + 1] != u);//边出栈边输出
cout << endl;
}
}
int main() {
cin >> n>> m;
for (int i = 0; i < m; i++) {
cin >> a >> b;
add(a, b, 0);
}
for (int i = 1; i <= n; i++) {
if (!DFN[i]) {
tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
}
}
return 0;
}
整个tarjan算法的流程大致就是这样,可能写的有点简陋,不过要是直接看懂伪代码之后再去理解文字再结合C++代码应该比较好理解,完毕。