转 tarjan算法求强连通分量
无意中想起图的强连通分量来,之前也一直想写所以今天决定来填这个坑。PS:由于本人比较懒,之前做过一个讲解的PPT,不过那是好遥远之前,年代已久早已失传,所以本文里的图来自网络。以后周末都用来填坑也挺好。
------------------------------分割线-----------------------------------------
在有向图G中,如果两个顶点间至少存在一条路径,那么这两个顶点就是强连通(strongly connected)。
如果有向图G的每两个顶点都强连通,称G是一个强连通图。
非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
例如:子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
![Tarjan算法与图的割点、桥与双连通分支 - 慕希颜 - 慕希颜的博客](http://img2.ph.126.net/xMLzAW4PawD5VLzQRh0xnA==/2838393665167128959.png)
求强连通分量的方法有多种,比如你可以用双向遍历取交集,不过复杂度稍高一点O(N^2+M)。我们现在来看一种复杂度为O(N+M)的算法:Tarjan算法。代码如下:
1 int index,bcnt; 2 int DFN[100],LOW[100],stack[100],belong[100]; 3 bool instack[100]; 4 5 void tarjan(int i) 6 { 7 int j; 8 DFN[i]=LOW[i]=++index; 9 instack[i]=true; 10 stack[++top]=i; 11 for (edge *e=V[i];e;e=e->next) 12 { 13 j=e->t; 14 if (!DFN[j])// 如果节点j未被访问过 15 { 16 tarjan(j); 17 if (LOW[j]<LOW[i]) 18 LOW[i]=LOW[j]; 19 } 20 else if (instack[j] && DFN[j]<LOW[i])// 如果节点j还在栈内 21 LOW[i]=DFN[j]; 22 } 23 if (DFN[i]==LOW[i])// 如果节点i是强连通分量的根 24 { 25 bcnt++; 26 do 27 { 28 j=stack[top--]; 29 instack[j]=false; 30 belong[j]=bcnt; 31 } 32 while (j!=i); 33 } 34 } 35 void solve() 36 { 37 int i; 38 top=bcnt=index=0; 39 memset(DFN,0,sizeof(DFN)); 40 for (i=1;i<=N;i++) 41 if (!DFN[i]) 42 tarjan(i); 43 }
Tarjan算法是基于对图的深度优先遍历,把每个强连通分量当做搜索树中的一棵子树。搜索时,把未处理的节点放到堆栈中,并借助时间戳在回溯时判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。
算法执行过程如下(PS:再次强调图片来自网络,其中栈的画的不是很好,自顶向下弹栈会更直观一点,结果图弄反了):
![Tarjan算法与图的割点、桥与双连通分支 - 慕希颜 - 慕希颜的博客](http://img0.ph.126.net/M-r_JRhwnwLDnMrJhZMyMg==/2965057404770210316.png)
从节点1开始进行深度优先遍历,把每个节点加入栈中并打上时间戳,也就是该节点是第几个被访问。当我们走到节点u=6时发现DFN[6]=LOW[6],那么便开始弹栈直到u=v为止,{6}为一个强连通分量。
![Tarjan算法与图的割点、桥与双连通分支 - 慕希颜 - 慕希颜的博客](http://img0.ph.126.net/uIZvvcCpjrG58WToYDRdCA==/4901605244539508778.png)
同理,返回节点5时也发现DFN[5]=LOW[5],继续弹栈,{5}为一个强连通分量。
![Tarjan算法与图的割点、桥与双连通分支 - 慕希颜 - 慕希颜的博客](http://img0.ph.126.net/ATQ_6w5r0625AQZudYjg9g==/4891190670401276625.png)
![Tarjan算法与图的割点、桥与双连通分支 - 慕希颜 - 慕希颜的博客](http://img0.ph.126.net/yN5RLLiHdEh1w3O74192kA==/2080100077991941158.png)
至此,算法结束。得到图中的三个强连通分量{1,3,4,2},{5},{6}。
以上便是Tarjan算法的执行过程。下面我们说一下图的割点、桥与双连通分支的问题。
在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。一个图的点连通度的定义为,最小割点集合中的顶点数。类似的,如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合。一个图的边连通度的定义为,最小割边集合中的边数。
如果一个无向连通图的点连通度大于1,则称该图是点双连通的(point biconnected),简称双连通或重连通。一个图有割点,当且仅当这个图的点连通度为1,则割点集合的唯一元素被称为割点(cut point),又叫关节点(articulation point)。
如果一个无向连通图的边连通度大于1,则称该图是边双连通的(edge biconnected),简称双连通或重连通。一个图有桥,当且仅当这个图的边连通度为1,则割边集合的唯一元素被称为桥(bridge),又叫关节边(articulation edge)。
在图G的所有子图G'中,如果G'是双连通的,则称G'为双连通子图。如果一个双连通子图G'它不是任何一个双连通子图的真子集,则G'为极大双连通子图。双连通分支(biconnected component),或重连通分支,就是图的极大双连通子图。特殊的,点双连通分支又叫做块。求割点与桥:
通过上面讲的Tarjan算法有:
Low(u)=Min { DFS(u) DFS(v)} 此时(u,v)为后向边(返祖边) 即等价于 DFS(v)<DFS(u)且v不为u的父亲节点
OR
Low(u)=Min{DFS(U) Low(v)} 此时(u,v)为树枝边(父子边)
一个顶点u是割点,则要么 u为树根,且u有多于一个子树。或者是 u不为树根,且满足存在(u,v)为父子边即u为v在搜索树中的父亲,使得DFS(u)<=Low(v)。
一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足DFS(u)<Low(v)。
求双连通分支:
对于求点双连通分支,建立一个栈,存储当前双连通分支,在遍历时,每找到一条树枝边或后向边,就把这条边加入栈中。如果遇到满足DFS(u)<=Low(v)时,说明u是一个割点,此时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
对于边双连通分支,只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
活在现实,做在梦里。