Tarjan算法的操作原理如下:
Tarjan算法基于定理:在任何深度优先搜索中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。也就是说,强连通分量一定是有向图的某个深搜树子树。
可以证明,当一个点既是强连通子图Ⅰ中的点,又是强连通子图Ⅱ中的点,则它是强连通子图Ⅰ∪Ⅱ中的点。
这样,我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值。注意,该子树中的元素在栈中一定是相邻的,且根节点在栈中一定位于所有子树元素的最下方。
强连通分量是由若干个环组成的。所以,当有环形成时(也就是搜索的下一个点已在栈中),我们将这一条路径的low值统一,即这条路径上的点属于同一个强连通分量。
如果遍历完整个搜索树后某个点的dfn值等于low值,则它是该搜索子树的根。这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。
算法框架
Dfn(u)为节点u搜索的次序编号(时间戳)。
Low(u)我们用low值记录该点所在强连通子图对应的搜索子树的根节点的Dfn值,为u或者u的子树能够追溯到的最早的栈中的节点的次序号dfn。
1.数组的初始化:当首次搜索到点p时,Dfn与Low数组的值都为到该点的时间。
2.堆栈:每搜索到一个点,将它压入栈顶。
3.当点u有与点v相连时,如果此时(时间为dfn[u]时)v不在栈中,u的low值为两点的low值中较小的一个。(这是顺着树边向下找)
4.当点u有与点v相连时,如果此时(时间为dfn[u]时)v在栈中,u的low值为u的low值和v的dfn值中较小的一个。(出现环了,这是一条返祖边,一个联通分量可能有多个环)
5.每当搜索到一个点经过以上操作后(也就是子树已经全部遍历)的low值等于dfn值,则将它以及在它之上的元素弹出栈。这些出栈的元素组成一个强连通分量。
6.继续搜索(或许会更换搜索的起点,因为整个有向图可能分为两个不连通的部分),直到所有点被遍历。
伪代码
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)
}
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。N为点数,M为边数。
#include<cstdio>
#include<algorithm>
#include<string.h>
#include<iostream>
using namespace std;
struct node {
int v,next;
} edge[1001];
int DFN[1001],LOW[1001];
int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y) {
edge[++cnt].next=heads[x];
edge[cnt].v = y;
heads[x]=cnt;
return ;
}
void tarjan(int x) { //代表第几个点在处理。递归的是点。
DFN[x]=LOW[x]=++tot;// 新进点的初始化。
cout<<x<<":"<<DFN[x]<<" "<<LOW[x]<<endl;
stack[++index]=x;//进站
visit[x]=1;//表示在栈里
for(int i=heads[x]; i!=-1; i=edge[i].next) {
if(!DFN[edge[i].v]) {//如果没访问过
cout<<" in u-v: "<<x<<"->"<<edge[i].v<<" low"<<x<<"="<<LOW[x]<<" low"<<edge[i].v<<"="<<LOW[edge[i].v]<<endl;
tarjan(edge[i].v);//往下进行延伸,开始递归
cout<<" out: "<<x<<"->"<<edge[i].v<<" low"<<x<<"="<<LOW[x]<<" ";
LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
cout<<" low"<<edge[i].v<<"="<<LOW[edge[i].v]<<"=> low"<<x<<":"<<LOW[x]<<endl;
}
else if(visit[edge[i].v ]) { //出现了环:如果访问过,并且还在栈里。
cout<<" else"<<edge[i].v<<":instack "<<x<<"->"<<edge[i].v<<" low"<<x<<"="<<LOW[x]<<" ";
LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。找出所有子树中最早出现的dfn
cout<<" dfn"<<edge[i].v<<"="<<DFN[edge[i].v]<<"=>low"<<x<<"="<<LOW[x]<<endl;
}
}
if(LOW[x]==DFN[x]) { //发现是整个强连通分量子树里的最小根。
cout<<"ssc:";
do {
printf(" %d dfn=%d, low=%d \n",stack[index],DFN[stack[index]],LOW[stack[index]]);
visit[stack[index]]=0;
index--;
} while(x!=stack[index+1]);//出栈,并且输出。
printf("\n");
}
return ;
}
int main() {
memset(heads,-1,sizeof(heads));
int n,m;
scanf("%d%d",&n,&m);
int x,y;
for(int i=1; i<=m; i++) {
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1; i<=n; i++){
cout<<i<<endl;
if(!DFN[i]) tarjan(i);//当这个点没有访问过,就从此点开始。防止图没走完
}
return 0;
}
数据:及执行结果
6 7
1 2
2 3
3 5
3 4
4 1
5 6
5 4
1
1:1 1
inu-v: 1->2 low1=1 low2=0
2:2 2
inu-v: 2->3 low2=2 low3=0
3:3 3
inu-v: 3->4 low3=3 low4=0
4:4 4
else1:instack 4->1 low4=4 dfn1=1=>low4=1
out: 3->4 low3=3 low4=1=> low3:1
inu-v: 3->5 low3=1 low5=0
5:5 5
else4:instack 5->4 low5=5 dfn4=4=>low5=4
inu-v: 5->6 low5=4 low6=0
6:6 6
ssc: 6 dfn[]=6, low[]=6
out: 5->6 low5=4 low6=6=> low5:4
out: 3->5 low3=1 low5=4=> low3:1
out: 2->3 low2=2 low3=1=> low2:1
out: 1->2 low1=1 low2=1=> low1:1
ssc: 5 dfn[]=5, low[]=4
4dfn[]=4, low[]=1
3dfn[]=3, low[]=1
2dfn[]=2, low[]=1
1dfn[]=1, low[]=1
codevs1332 上白泽慧
Poj2186 Popular Cows