Tarjan算法
首先我们需要知道强连通分量,因为Tarjan就是用来求这个的。连通的意思是对于<u,v>,存在路径可以从其中一个达到另一个;强连通是指即存在从u到v的路径,又存在从v到u的路径。强连通是针对于有向图来说的,因为无向图只要做到了连通就做到了强连通。强连通分量就是指有向图的极大强连通子图,多一个点少一个点都不能保证强连通。
求一个有向图的强连通分量有三种常用算法,Kosaraju算法、Tarjan算法、Gabow算法。其中最最常用且效率很高的是Tarjan算法。
Tarjan算法利用了dfs,定义dfn[u]表示结点u的时间戳(第几个被访问的),low[u]表示从结点u向下遍历所能找到的最小的dfn。强连通分量里必然存在环,从其中一个结点向下遍历必然会回到自身,而low中存放的是找到最小的dfn,假若一个结点的dfn=low,那么他一定是强连通分量中第一个被访问到的点。如果我们使用栈保存被访问过的结点,那么从这个结点开始往上直到栈顶就是当前强连通分量中的点。
如果我们把所有的强连通分量找出来,缩成一个点,那么这个图将变成DAG,因为不再有环。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<stack> 5 using namespace std; 6 int n,m,head[maxn],eid; 7 struct edge { 8 int v,next; 9 } E[maxm]; 10 void insert(int u,int v) { 11 E[eid].v=v; 12 E[eid].next=head[u]; 13 head[u]=eid++; 14 } 15 int dfn[maxn],low[maxn],ins[maxn],vid,scc,belong[maxn]; 16 stack<int> s; //用栈存放已访问的结点 17 void tarjan(int u) { 18 dfn[u]=low[u]=vid++; //初始化,打时间戳 19 s.push(u); //结点入队,同时记录结点是否在队列中 20 ins[u]=1; //不在队列中可视为属于其他强连通分量 21 for(int p=head[u];p+1;p=E[p].next) { 22 int v=E[p].v; 23 if(!dfn[v]) { //对于未访问过的结点 24 tarjan(v); 25 low[u]=min(low[u],low[v]); //更新最早的时间戳 26 } else if(ins[v]) low[u]=min(low[u],dfn[v]); 27 } //若v在栈中,说明属于当前强连通分量,也要考虑他的时间戳 28 //最后low里就是从该结点出发能找到的最小的时间戳 29 if(dfn[u]==low[u]) { //说明该结点是当前强连通分量最早被标记的点 30 ++scc; 31 while(!s.empty()) { 32 int t=s.top();s.pop(); 33 ins[t]=0; //记得更新ins 34 belong[t]=scc; 35 if(t==u) break; 36 } 37 } 38 }
只从一个结点出发并不一定能找全图上所有的SCC,所以需要进行多次Tarjan。
for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);