图的强连通
定义1 如果有向图G的任何两顶点都互相可达,则称图G是强连通图,如果有向图G存在两顶点u和v,使得u不能到达v或则v不能到达u,则称图G是非强连通图。
定义2 如果有向图G不是强连通图,他的子图G‘是强连通图,点v属于G’,任意包含v的强连通子图也是G‘的子图,则称G’是有向图G的极大强连通子图。
下面介绍强连通分量的三种算法:Kosaraju、Tarjan和Garbow算法。
Kosaraju算法
基本算法:
Kosaraju算法主要是对原图G和反图GT分别进行一次dfs。
(1)对原图进行dfs形成森林(树)。
(2)然后任选一棵树对其反图按其第一次搜索回溯的逆序进行dfs,能够遍历到的节点就是一个强连通分量。余下部分和原来的森林一起组成一个新的森林,继续步骤(2)直到没有顶点为止。
问题与改进:
由强连通定义可知,强连通分量只能存在于单棵树中,而基本思路的步骤(2)每次对一棵树进行dfs时,可能搜到其他树上去,这是不允许的。可以通过巧妙地选择第二次dfs的顺序,使其不会搜到其他树上去。如果步骤(2)从森林里选择树时从最后遍历出来的树开始深度优先搜索,就不可能连通到前面深度优先搜索到的树。而且最后遍历出来的树,他的根节点在步骤(1)的遍历中离开时间最晚,可知他也是该树中离开时间最晚的那个节点。在第一次dfs时,记录时间i离开的节点j,即num[i]=j。那么,每次只需找到没有找过的顶点中具有最晚离开时间的顶点直接对GT进行深度优先搜索就可以了。每次深度优先搜寻都得到一个强连通分量。
代码如下:
1 const int maxn=110; 2 struct node 3 { 4 int t,next,f,next2; 5 }edge[maxm+10];//链式前向星 6 7 int dfsone(int cur,int &sig) 8 { 9 vis[cur]=true; 10 for(int i=head[cur];i!=-1;i=edge[i].next) 11 { 12 if(!vis[edge[i].t])dfsone(edge[i].t,sig); 13 } 14 num[++sig]=cur; 15 return 0; 16 } 17 18 int dfstwo(int cur,int sig) 19 { 20 vis[cur]=true; 21 Scc[cur]=sig; 22 for(int i=head2[cur];i!=-1;i=edge[i].next2) 23 { 24 if(!vis[edge[i].f]) 25 { 26 dfstwo(edge[i].f,sig); 27 } 28 } 29 return 0; 30 } 31 int Kosaraju() 32 { 33 int sig=0; 34 memset(vis,0,sizeof(vis)); 35 for(int i=1;i<=n;i++) 36 { 37 if(!vis[i]) 38 { 39 dfsone(i,sig); 40 } 41 } 42 sig=1; 43 memset(vis,0,sizeof(vis)); 44 for(int i=n;i>0;i--) 45 { 46 if(!vis[num[i]]) 47 { 48 dfstwo(num[i],sig++); 49 } 50 } 51 return sig; 52 }
该算法的时间复杂度和空间复杂度都是O(n+m)。
其实也可以不求反图。
具体流程如下:
(1)任意对图进行一次dfs,得到每个节点遍历结束时的时间end[i]。
(2)按照end[i]从小到大的顺序给节点排序。
(3)按照(2)中的节点顺序,再次对图进行dfs,每次新得到一棵dfs树,就对应一个强连通块。
Tarjan算法
基本算法:
如果对原图进行深度优先搜索,由强连通分量定义可知,任何一个强连通分量是原图的深度优先搜索树的子树。那么只要确定每个强连通分量子树的根,然后根据这些根从树的最底层开始,一个一个地取出强连通分量即可。
对于确定强连通分量的根,在这里维护两个数组,一个是dfn[maxn],一个是low[maxn],其中dfn[v]记录的是节点i在深度优先遍历序列中的访问次序;low[v]记录的是与点v邻接的未删除的顶点u的low[u]和low[v]的最小值(初始化为dfn[v])。这样,在一次深度优先搜索的回溯过程中,如果发现low[v]==dfn[v](表示他不能到达他的任何一个祖先),那么当前顶点就是一个强连通分量的根。因为如果不是强连通分量的根,那么一定是属于另一个强连通分量,而且他的根是当前节点的祖宗,那么存在包含当前节点的到其祖宗的回路,可知low[v]可以被更改为一个比dfn[v]小的值。
对于如何取出强连通分量,这个比较简单,如果当前节点为一个强连通分量的根,那么他的强连通分量一定是以该根为根节点的(剩下节点)子树。在深度优先遍历的时候维护一个堆栈,每次访问一个新节点,就压入堆栈。由于当前点是这个强连通分量中最先被压入堆栈的,那么在当前节点以后压入堆栈的并且仍在堆栈中的节点都属于这个强连通分量。假设一个节点在当前节点压入堆栈以后压入并且还存在,同时不属于该强连通分量,那么一定属于另一个强连通分量,但当前节点是其根的祖宗,那么这个强连通分量应该在此之前已经被取出。
代码如下:
1 const int maxn=110; 2 struct node 3 { 4 int t,next; 5 }edge[maxm+10];//链式前向星 6 int m; 7 8 int tardfs(int k,int lay,int &scc_num) 9 { 10 vis[k]=1; 11 low[k]=lay; 12 dfn[k]=lay; 13 Stack[++m]=k; 14 for(int i=head[k];i!=-1;i=edge[i].next) 15 { 16 if(!vis[edge[i].t]) 17 { 18 tardfs(edge[i].t,++lay,scc_num); 19 } 20 if(vis[edge[i].t]==1)low[k]=min(low[k],low[edge[i].t]); 21 } 22 if(dfn[k]==low[k]) 23 { 24 ++scc_num; 25 do 26 { 27 low[Stack[m]]=scc_num; 28 vis[Stack[m]]=2; 29 }while(Stack[m--]!=k); 30 } 31 return 0; 32 } 33 34 int tarjan(int n) 35 { 36 int scc_num=0,lay=1; 37 m=0; 38 memset(vis,0,sizeof(vis)); 39 memset(low,0,sizeof(low)); 40 for(int i=1;i<=n;i++) 41 { 42 if(!vis[i])tardfs(i,lay,scc_num);//这里lay=1,表示不同的树 43 } 44 return scc_num; 45 }
时间复杂度:O(n+m)。
NOTE:low[v]实际上等于v所能到达的最早被访问的祖先的low值。
Garbow算法
Garbow算法是Tarjan算法的另一种实现,不同的是Garbow算法在编程时用一个堆栈来辅助求出强连通分量的根。
使用类比的方法,在Tarjan算法中,每次low[v]的修改都是由于环的出现,不然,low[v]的值不可能变小。每次出现环时,在这个环里只剩下一个low[v]没有被改变(深度最低的那个),或者全部被改变,如果那个深度最低的节点在另一个环里。那么Garbow算法中的第二个堆栈变化就是删除构成环的节点,只剩深度最低的节点,或者全部删除。这个过程通过出栈来实现。因为深度最低的那个点一定比前面的先访问,那么只要出栈直到栈顶那个顶点的访问时间不大于深度最低的那个顶点。其中每个被弹出的节点属于同一个强连通分量。因为在这个节点访问之前,能够构成强连通通量的那些节点已经被弹出了。
Garbow算法和Tarjan算法是在同一种思想下的不同实现,但是Garbow算法更加精妙,时间更少,不用频繁更新low。
代码如下:
1 const int maxn=110; 2 struct node 3 { 4 int t,next; 5 }edge[maxm+10]; 6 int Garbowdfs(int cur,int lay,int &scc_num) 7 { 8 sta1[++m1]=cur;sta2[++m2]=cur; 9 low[cur]=++lay; 10 for(int i=head[cur];i!=-1;i=edge[i].next) 11 { 12 if(!low[edge[i].t]) 13 { 14 Garbowdfs(edge[i].t,lay,scc_num); 15 } 16 else if(belg[edge[i].t]==0) 17 { 18 while(low[sta2[m2]]>low[edge[i].t]) 19 --m2; 20 } 21 } 22 if(sta2[m2]==cur) 23 { 24 m2--; 25 scc_num++; 26 do 27 { 28 belg[sta1[m1]]=scc_num; 29 }while(sta1[m1--]!=cur); 30 } 31 return 0; 32 } 33 int Garbow() 34 { 35 int scc_num=0,lay=0; 36 memset(belg,0,sizeof(belg)); 37 memset(low,0,sizeof(low)); 38 for(int i=0;i<n;i++) 39 if(low[i]==0)Garbowdfs(i,lay,scc_num); 40 return scc_num; 41 }
时间复杂度:O(n+m)。
参考文献:《图论及应用》哈尔滨工业大学出版社
特此申明:严禁转载
2014-02-22