天天看点

【学习】【Tarjan】【强连通分量】

       正题之前唠唠几句,写这篇博客的原因是因为做到了一道图论题,发现显然需要缩点解决,但是发现自己图论中的缩点基本忘完了,同时与之相关的Tarjan求SCC(强连通分量Strong Connected Component)也不记得多少,才想着把图论当中的Tarjan求SCC复习一遍。

       附上Tarjan求割边割点的学习博客:https://blog.csdn.net/yanyidu/article/details/79793736

正题:

       首先,先交代一大串概念(一定要熟记!)。

       1.图的表示:我们用V表示图G的顶点的集合,E表示图G的边的集合,那么我们图G=(V,E)。

       2.连通图:在无向图中,如果任意两点间可相互到达则称该图为连通图。

       3.子图:如果存在图G=(V,E),G1=(V1,E1),并且V1,E1分别是V,E的子集,那么我们称G1是G的子图。

       4.连通子图:如果G的子图是连通图,则称该子图为连通子图。

       5.连通分量与最大连通子图:无向图G的最大连通子图称为G的连通分量;如果G1是G的连通子图,将G的任何不在该子图中的点加入到G1后,G1将不再连通,那么就称G1是G的最大连通子图。

       6.强连通图:在有向图中,如果任意两点之间可相互到达,则称该图为强连通图。(强连通图与连通图的区别就在于有向图与无向图)

       7.强连通子图:如果有向图G的子图G1是强连通图,则称G1为G的强连通子图

       8.强连通分量与最大强连通子图:有向图G的最大强连通子图称为G的强连通分量;如果G1是G的强连通子图,将G的任何不在该子图的点加入到G1后,G1将不再连通,那么就称G1是G的最大强连通子图。

       然后,理清楚上面的一些概念之后我们就开始来讲讲如何求出一个图强连通分量的个数(由于我们只学习如何求出一个图强连通分量的个数,所以似乎只需要理解什么是强连通分量即可?)

       也许到了这里,大家松了一口气说终于没有需要记忆的概念了。如果这么想,那就错了。接下来我们还要介绍一个图中的点的DFN值与LOW值。

       先搬出DFN与LOW的概念:我们用DFS来遍历一个有向图,在遍历的过程中,用DFN[I]表示编号为I的节点在DFS过程中的访问序号,用LOW[I]表示节点I及其下方节点(也就是有向边的终点节点)所能到达的访问序号最小的节点的访问序号,显然初始DFN[I]=LOW[I]。我们在通过DFS遍历的过程中显然会得到一棵搜索树。在搜索树上越上层的节点,显然DFN的值就越小。遍历过程中如果发现某节点在原图中有边连到搜索树中自己的祖先节点,就更新LOW值。

       没有懂?那我们来看一下下面的例子。

       比如这一张图:

【学习】【Tarjan】【强连通分量】

      我们不妨以a为起点开始遍历,于是得到下面的搜索树。

【学习】【Tarjan】【强连通分量】

       在搜索树图中紫色的数字就是每个点的访问顺序,也就是对应的DFN值,那么LOW值呢?

       为了加深对于LOW值的理解,我们来简单模拟一下LOW值的更新:

       初始化:LOW[I]=I;

       我们从1(a,b,...,h分别对应1,2,...,8)开始搜,1的有向边的终点2,显然LOW[2]>LOW[1],所以不更新LOW[1],同理从2搜索到3不更新,一直搜索到4的有向边的终点5,这个时候会发现我们并没有更新任何的LOW值,但是接下来问题就来了。我们根据原图发现5有一条有向边终点是3,这个时候我们发现5的这条有向边连向了自己的某一个最先节点,根据我们上面已经介绍的,就需要更新LOW[5]了,也就是说此时LOW[5]=min(LOW[5],LOW[3])=LOW[3]=3,由于5能够通过图中的一条返祖边到达3,而4可以到达5,于是我们也要更新LOW[4],也就是LOW[4]=min(LOW[4],LOW[5])=LOW[5]=3。这个时候我们就返回到了3号点,发现从3号点出发的有向边已经讨论完了,那么这个时候就顺序返回到自己的一个没有讨论完所有有向边的祖先节点,也就是1号点了。此时1还存在以1为起点的有向边没有讨论完,我们顺次讨论即可。

      此时,我们就已经分析完了整个通过DFS遍历图的过程了,得到如下的LOW值:

      LOW[1]=1,LOW[2]=2,LOW[3]=LOW[4]=LOW[5]=3,LOW[6]=1,LOW[7]=LOW[8]=1

      于是我们这个时候花了相当精力在DFN值与LOW值上,那么这两个值有什么用呢?当然是用来帮助我们求目标强连通分量!

      DFN值的简单应用:在一个有根树上,怎样用O(1)的时间判断一个结点X是否是另一个结点Y的祖先?我们只需要从根节点出发通过DFS遍历整棵树,并且在遍历的过程中记录每个节点的DFN值和儿子节点数量就可以了。假设X有K个儿子节点,显然如果满足这个条件,那么X就是Y的祖先节点:

【学习】【Tarjan】【强连通分量】

      通过上述的介绍,相信不难得出如下结论:

      如果一个节点的LOW值小于DFN值,那么就说明它或其子孙节点有边连到自己上方的节点

      如果一个节点的LOW值等于DFN值,那么久说明它下方的节点不能走到它上方的节点,也就是说那么该节点就是一个强连通分量在DFS搜索树中的根。

      那么问题就来了: X的子孙节点未必与X处于同一个强连通分量,那么应该如何找到在X的子孙节点中,有哪些点是与X位于同一个强连通分量当中呢?对于这个问题我们用栈解决就行了。

给出Tarjan求强连通分量的代码:

//Num记录已经讨论到了几个点。
//Scc记录已经找到的强连通分量的个数
//Ins[I]表示I点是否在栈中,即是否正被讨论当中
//Bel[I]表示I点属于哪一个强连通分量 
stack<LL>S;
void Tarjan(LL X){
	DFN[X]=LOW[X]=++Num;
	S.push(X);Ins[X]=true;
	for(LL Y=1;Y<=N;Y++){
		if(Map[X][Y]&&DFN[Y]==0){
			Tarjan(Y);
			LOW[X]=min(LOW[X],LOW[Y]);
		} else if(Map[X][Y]&&DFN[Y]!=0&&Ins[Y]){
			LOW[X]=min(LOW[X],LOW[Y]);
		}
	} 
	if(DFN[X]==LOW[X]){
        ++SCC;  
        Y=S.top();S.pop();Bel[Y]=SCC;  
        while(X!=Y){  
            Y=S.top();S.pop();Bel[Y]=SCC;  
        }  
    }  
}
           

       但是仅仅是这样的做法其实不能保证讨论完每一个点,因为原图不一定是一个连通图,这个时候我们就需要一个调用窗口:

for(LL I=1;I<=N;I++){
    if(!Bel[I]){
        Tarjan(I);
    }
}
           

       至此,我们就讲完了如何用Tarjan来求出一个图的SCC个数以及每个SCC都有哪些点(什么?你什么时候讲了Tarjan?其实上面整个DFN与LOW相关都是Tarjan算法的一部分)。

       上述如有错误,恭请请教!

继续阅读