Tarjan算法详解
【概念】
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
【功能】
Tarjan算法的用途之一是,求一个有向图G=(V,E)里极大强连通分量。强连通分量是指有向图G里顶点间能互相到达的子图。而如果一个强连通分量已经没有被其它强通分量完全包含的话,那么这个强连通分量就是极大强连通分量。
【算法思想】
用dfs遍历G中的每个顶点,通dfn[i]表示dfs时达到顶点i的时间,low[i]表示i所能直接或间接达到时间最小的顶点。(实际操作中low[i]不一定最小,但不会影响程序的最终结果)
程序开始时,time初始化为0,在dfs遍历到v时,low[v]=dfn[v]=time++,
v入栈(这里的栈不是dfs的递归时系统弄出来的栈)扫描一遍v所能直接达到的顶点k,
如果 k没有被访问过那么先dfs遍历k,low[v]=min(low[v],low[k]);
如果k在栈里,那么low[v]=min(low[v],dfn[k])(就是这里使得low[v]不一定最小,但不会影响到这里的low[v]会小于dfn[v])。
扫描完所有的k以后,如果low[v]=dfn[v]时,栈里v以及v以上的顶点全部出栈,且刚刚出栈的就是一个极大强连通分量。
【大概的证明】
1.在栈里,当dfs遍历到v,而且已经遍历完v所能直接到达的顶点时,low[v]=dfn[v]时,v一定能到达栈里v上面的顶点:
因为当dfs遍历到v,而且已经dfs递归调用完v所能直接到达的顶点时(假设上面没有low=dfn),这时如果发现low[v]=dfn[v],栈上面的顶点一定是刚才从顶点v递归调用时进栈的,所以v一定能够到达那些顶点。
2 .dfs遍历时,如果已经遍历完v所能直接到达的顶点而low[v]=dfn[v],我们知道v一定能到达栈里v上面的顶点,这些顶点的low一定小于 自己的dfn,不然就会出栈了,也不会小于dfn[v],不然low [v]一定小于dfn[v],所以栈里v以其v以上的顶点组成的子图是一个强连通分量,如果它不是极大强连通分量的话low[v]也一定小于dfn[v](这里不再详细说),所以栈里v以其v以上的顶点组成的子图是一个极大强连通分量。
【时间复杂度】
因为所有的点都刚好进过一次栈,所有的边都访问的过一次,所以时间复杂度为O(n+m)
【算法演示】
下面给出一个大牛写的tarjan算法演示,很好,将tarjan算法的操作原理形象地表现了出来,可以很好地理解整个算法的执行过程。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。
求有向图的强连通分量还有一个强有力的算法,为Kosaraju算法。Kosaraju是基于对有向图及其逆图两次DFS的方法,其时间复杂度也是O(N+M)。与Trajan算法相比,Kosaraju算法可能会稍微更直观一些。但是Tarjan只用对原图进行一次DFS,不用建立逆图,更简洁。在实际的测试中,Tarjan算法的运行效率也比Kosaraju算法高30%左右。此外,该Tarjan算法与求无向图的双连通分量(割点、桥)的Tarjan算法也有着很深的联系。学习该Tarjan算法,也有助于深入理解求双连通分量的Tarjan算法,两者可以类比、组合理解。
求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法,以及求最近公共祖先的离线Tarjan算法,在此对Tarjan表示崇高的敬意。
【源代码c++】
1 #include <iostream> 2 #include <stack> 3 using namespace std; 4 5 #define MAX_VERTEX_SIZE 10001 6 struct EdgeNode{ 7 int vertex; 8 EdgeNode *nextArc; 9 }; 10 11 struct VerTexNode{ 12 EdgeNode* firstArc; 13 }; 14 15 struct Graph{ 16 int n,e; 17 VerTexNode vNode[MAX_VERTEX_SIZE]; 18 }; 19 20 int time = 0; 21 int low[MAX_VERTEX_SIZE]; 22 int dfn[MAX_VERTEX_SIZE]; 23 int visited[MAX_VERTEX_SIZE]; 24 int inStack[MAX_VERTEX_SIZE]; 25 stack<int> st; 26 Graph graph; 27 28 void initeGraph(int n,int m) 29 { 30 for(int i = 1;i<=n;i++) 31 { 32 graph.vNode[i].firstArc = NULL; 33 } 34 graph.n = n; 35 graph.e = m; 36 37 } 38 39 //头插法建立图 40 void creatGraph(int s,int v) 41 { 42 EdgeNode *edgeNode = new EdgeNode; 43 edgeNode->vertex = v; 44 edgeNode->nextArc = graph.vNode[s].firstArc; 45 graph.vNode[s].firstArc = edgeNode; 46 } 47 48 int min(int a,int b) 49 { 50 if(a>b) 51 return b; 52 else 53 return a; 54 } 55 56 void trajan(int u) 57 { 58 dfn[u] = low[u] = time++; 59 st.push(u); 60 visited[u] = 1; 61 inStack[u] = 1; 62 EdgeNode *edgePtr = graph.vNode[u].firstArc; 63 while(edgePtr !=NULL) 64 { 65 int v = edgePtr->vertex; 66 if(visited[v] == 0) 67 { 68 trajan(v); 69 low[u] = min(low[u],low[v]); 70 } 71 else 72 { 73 low[u] = min(low[u],dfn[v]); 74 } 75 edgePtr = edgePtr->nextArc; 76 } 77 78 if(dfn[u] == low[u]) 79 { 80 int vtx; 81 cout<<"set is: "; 82 do{ 83 vtx = st.top(); 84 st.pop(); 85 inStack[vtx] = 0;//表示已经出栈 86 cout<<vtx<<' '; 87 }while(vtx !=u ); 88 } 89 90 } 91 92 int main() 93 { 94 int n,m; 95 int s,a; 96 cin>>n>>m; 97 initeGraph(n,m); 98 for(int i = 1;i<=n;i++) 99 { 100 visited[i] = 0; 101 inStack[i] = 0; 102 dfn[i] = 0; 103 low[i] = 0; 104 } 105 106 for(int j = 1;j<=m;j++) 107 { 108 cin>>s>>a; 109 creatGraph(s,a); 110 } 111 112 for(int i =1;i<=n;i++) 113 if(visited[i] == 0) 114 trajan(i); 115 return 0; 116 }