C++学习笔记:Tarjan算法剖析——求 强连通分量,割点,割边,点双连通分量,边双连通分量 的详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_44013342/article/details/90715209

Tarjan算法详解

目录

1.Tarjan算法求强连通分量

2. Tarjan算法求割点

3. Tarjan算法求点双连通分量

4. Tarjan算法求割边

5. Tarjan算法求边双连通分量


1.Tarjan算法求强连通分量

了解一下 强连通分量

对于一个有向图的DFS的搜索树(i 可以到 j,j 不一定能到 i),如下

里面的强连通分量有 { 6 , 7 , 8 } ,{ 4 } , { 3 } , { 2 } , { 1 }  , { 9 }

而强连通分量产生的环 { 6 , 7 , 8 } 是有父子关系的,所以在 { 2 , 3 , 4 , 9  } 这个环不是强连通分量,因为搜索树是有向的(3 , 9 可以到 4 , 而 4 不能访问回去)

在用Tarjan算法时,栈中的点一定是有父子关系的


DFN[ i ] 数组表示遍历到点 i 时DFS的次数

low[ i ] 数组表示点 i 到栈中


在搜索的过程中会先搜索 1——2——3——4,然后在到 9 的时候就会有这种情况

在用Tarjan算法时,用栈存储的点有 { 1 , 2 , 3 , 9 } ,这时还未遍历点 4,点 4 不在栈中,与点 9 没有父子关系

遍历到点 4 ,此时点 4 无法往下遍历,且与栈中点 9 无父子关系,只能退出栈,有强连通分量 { 4 },然后回溯

依次退栈,有强连通分量 { 3 },{ 9 },{ 2 },{ 1 }


遍历到如下情况

遍历到点 7,栈中的点有 { 5, 6 , 7 }

往下遍历到点 8,栈中点 { 5, 6,7,8 },到点 8 后往下遍历到点 6,点 6 为栈中点,则点 8 到栈中深度最小的点为点 6,low[ 8 ] = 6 

low[8] < low[7],说明点 6 可以到点 8,点 8 也可以到点 6 ,这其中的点都可以相互到达

然后就将放入栈中的点 8,7 取出(按入栈顺序),最后取出点 6 自己时停止

则 { 6,7 , 8 } 为强连通分量

Tarjan算法求强连通分量 主要代码

void Tarjan( int x ) {
	num ++ ;
	DFN[x] = low[x] = num ;//num表示在栈中的编号
	inS[x] = 1 ;
	S.push( x ) ; 
	for( int i = 0 ; i < G[x].size() ; ++ i ) {//搜索相连节点 
		int s = G[x][i] ;
		if( !DFN[s] ) {//没搜索过 
			Tarjan( s );
			low[x] = min( low[x] , low[s] );//更新所能到的上层节点 
		}
		else if( inS[s] ) {//在栈中 
			low[x] = min( low[x] , DFN[s] );//到栈中最上端的节点 
		}//DFN是栈中编号或时间戳,如果s在栈中,则x到栈中最上端节点为DFN[s]
	}
	if( low[x] == DFN[x] ) {
		cnt ++ ;
		int y ;
		do{//用 do_while 避免自己没被处理 
			y = S.top() ;
			inS[y] = 0 ;
			S.pop() ;
			K[y] = cnt ;
		}while( y != x );
	}
	return ;
}

Tarjan算法求强连通分量 模板

​
​
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <stack>
using namespace std;
 
int n , m ;// n 个点,m 条边 
int cnt , K[1005] , num ;// cnt强连通分量 ,K表示每个节点所属的强连通分量  
int DFN[1005] , low[1005] ;
bool inS[1005] ;//vis表示是否访问,inS表示是否在栈中 
vector<int> G[1005] ;
stack<int> S ;
int from[2005] , to[2005] ;//存边  
 
void init() {
	memset( inS , 0 , sizeof(inS) );
	memset( K , 0 , sizeof(K) );
	memset( DFN , 0 , sizeof(DFN) );
	memset( low , 0 , sizeof(low) );
	memset( from , 0 , sizeof(from) );
	memset( to , 0 , sizeof(to) );
	memset( L , 0 , sizeof(L) );
	cnt = 0 ;
	num = 0 ;
	while( !S.empty() )
		S.pop() ;
}
 
void Tarjan( int x ) {
	num ++ ;
	DFN[x] = low[x] = num ;//num表示在栈中的编号
	inS[x] = 1 ;
	S.push( x ) ; 
	for( int i = 0 ; i < G[x].size() ; ++ i ) {//搜索相连节点 
		int s = G[x][i] ;
		if( !DFN[s] ) {//没搜索过 
			Tarjan( s );
			low[x] = min( low[x] , low[s] );//更新所能到的上层节点 
		}
		else if( inS[s] ) {//在队中 
			low[x] = min( low[x] , DFN[s] );//到栈中最上端的节点 
		}//DFN是栈中编号或时间戳,如果s在栈中,则x到栈中最上端节点为DFN[s]
	}
	if( low[x] == DFN[x] ) {
		cnt ++ ;
		int y ;
		do{//用 do_while 避免自己没被处理 
			y = S.top() ;
			inS[y] = 0 ;
			S.pop() ;
			K[y] = cnt ;
		}while( y != x );
	}
	return ;
}
 
int main() {
	while( scanf("%d%d", &n, &m ) != EOF ) {
		init();//初始化 
		for(int i = 1 ; i <= m ; ++ i ) {//输入 
			int a , b ;
			scanf("%d%d", &a , &b );
			G[a].push_back(b); 
			from[i] = a , to[i] = b ;
		}
		for(int i = 1 ; i <= n ; ++ i ) {//找强连通分量 
			num = 0 ;
			if( !DFN[i] )
				Tarjan( i );
		}
		printf("%d\n", cnt );
		for(int i = 1 ; i <= n ; ++ i )
			G[i].clear() ;
	}
	return 0;
}

​

​

2. Tarjan算法求割点

割点是在无向图的

一棵无向图的搜索树(i 和 j 可以互相到达),如下

一个割点一定会在 1——2——3——4——9 或 1——5——6——7——8

求割点,可以这样理解

点 1,2,3,点 2 可以直接到点 1,而点 3 必须通过点 2 到点 1,则点 2 是割点

即点 3 没有一个不经过点 2 到点 1 的路径

即 low[ 3 ] > DFN[ 2 ]

所以对于 点  x,点 y

如果点 x 可以不经过点 y 跳出 y 的子树,则 y 不是割点

如何判断点 1 是割点,直接看点 1,有多少子节点,如果只有一个,则点 1 不是割点

主要代码

就不上模板了

void Tarjan( int x , int fa ) {
	num ++ ;
	DFN[x] = low[x] = num ;
	for( int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i] ;
		if( !DFN[s] ) {
			Tarjan( s , x );
		if( low[s] >= DFN[x] )
			isC[x] = 1 ;
		low[x] = min( low[x] , low[s]);
		}
		else if( DFN[x] > DFN[s] && s != fa ) {
			low[x] = min( DFN[s] , low[x] );
		}
	}
	
}

3. Tarjan算法求点双连通分量

了解割点后就直接看点双,一个无向图如下

有点双连通分量  { 2,3,4,9 } ,{ 6,7 , 8 } ,{ 1 , 2 },{ 1,5 } ,{ 5,6 } 

其搜索树为

先搜索点 1,

然后先搜索点 2,用一个栈 S 存储搜的点

再依次搜索点 3,4 , 9,栈中点有 1,2,3,4,9

发现点 2 是割点,有一个点双{ 2,3,4,9 }

这时将栈中点弹出,不过只弹出点3,4,9,因为点 2 也许与其他点又构成一个点双

然后回到点 1,栈中点{ 1 , 2 },点 1 为割点,构成点双{ 1 , 2 },弹出点 2

再搜索点 5——6——7——8

先发现点 6是割点,栈中点 { 1,2,5,6,7,8 }

点 6,7,8构成点双 { 6,7,8 },弹出点 {7,8},栈中点{ 1,5,6 }

回到点 5,发现点 5 是割点,点 5,6 构成点双,弹出点 6

回到点 1,发现点 1 是割点,点 1,5 构成点双,弹出点 5

找完了点双

总结一下

1.我们将点按照访问顺序入栈

2.当我们确定 x 是割点,即 x 的某个子节点 y 满足l ow[ y ] ≥ dfn [ x ] 时,我们将栈中的点依次弹出,直到栈顶为 x,x 和我们弹出的这些点构成了一个点双连通分量。注意:x 不能弹出,因为 x 可能属于多个点双连通分量。

3.如果x是根,即使不是割点也作如上处理。

(基本方法,copy不解释)

主要代码

void Tarjan( int x , int fa ) {
	num ++ ;
	DFN[x] = low[x] = num ;
	int kid = 0 ;
	S.push(x) ;//入栈
	for(int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i] ;
		if( !DFN[s] ) {
			kid ++ ;
			Tarjan( s , x );
			if( low[s] >= DFN[x] ) {//找到点双
				isC[x] = 1 ;
				cnt ++ ;
				D[cnt].push_back( x );
				while( x != S.top() ) {//存进去
					D[cnt].push_back( S.top() );// vector——D 用来存点双
					S.pop();
				}
			}
			low[x] = min( low[s] , low[x] );
		}
		else if( DFN[x] > DFN[s] && s != fa ) 
			low[x] = min( DFN[s] , low[x] ); 
	}
	if( fa == 0 && kid == 1 )//根节点只有一个子节点
		isC[x] = 0 ;
	if( fa == 0 && kid == 0 ) {//独立的点也是一个点双
		cnt ++ ;
		D[cnt].push_back( x ) ;
	}
}

模板

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#include <queue>
#include <stack>
#define ll long long
using namespace std;

int n , m , num , cnt ;
ll ans , sum ;
int bel[1005] , cut , G_num ;
int DFN[1005] , low[1005] ;
bool isC[1005] ;
vector <int> G[1005] ;
stack <int> S ;
vector<int> D[1005] ;

void Tarjan( int x , int fa ) {
	num ++ ;
	DFN[x] = low[x] = num ;
	int kid = 0 ;
	S.push(x) ;//入栈
	for(int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i] ;
		if( !DFN[s] ) {
			kid ++ ;
			Tarjan( s , x );
			if( low[s] >= DFN[x] ) {//找到点双
				isC[x] = 1 ;
				cnt ++ ;
				D[cnt].push_back( x );
				while( x != S.top() ) {//存进去
					D[cnt].push_back( S.top() );// vector——D 用来存点双
					S.pop();
				}
			}
			low[x] = min( low[s] , low[x] );
		}
		else if( DFN[x] > DFN[s] && s != fa ) 
			low[x] = min( DFN[s] , low[x] ); 
	}
	if( fa == 0 && kid == 1 )//根节点只有一个子节点
		isC[x] = 0 ;
	if( fa == 0 && kid == 0 ) {//独立的点也是一个点双
		cnt ++ ;
		D[cnt].push_back( x ) ;
	}
}

int main() {
	scanf("%d%d", &m , &n );
		for( int i = 1 ;  i <= n ; ++ i ) {
			int a , b ;
			scanf("%d%d", &a , &b );
			G[a].push_back(b);
			G[b].push_back(a);
			m = max( m , max(a,b) );
		}
	for( int i = 1 ; i <= m ; ++ i ) {
		if( !DFN[i] )
			Tarjan( i , 0 ) ;
	}
	for(int i = 1 ; i <= cnt ; ++ i ) {
		for( int j = 0 ; j < D[i].size() ; ++ j )
			printf("%d ", D[i][j] );
		printf("\n") ;
	}
}


4. Tarjan算法求割边

割边就是所谓的桥,其求法与割点相似,先把每一条边标号

一个如下的无向图

割边有边 1 , 6 , 7 

思路与割点一样,如果一个点 x 不可以不通过 边 L 到达点 y ,则边 L 是一条割边

然后它的搜索树为

每一条边都有一个编号

然后进行搜索

搜索1——2——3——4——9

一波搜索到了点 9,点 9 的 low[ 9 ] = 2

则边 5 不是割边,回溯到点 4 ,low[ 4 ] 更新为 low[ 9 ],low[ 4 ] = 2 ,说明点 4 能不通过边 3 跳出点 3 的子树

边 3 不是割边,回溯到点 3,发现 low[ 3 ] = 2,点3 可以不通过 边 2 到 其父节点 点 2,边 2 不是割边(所以判定是 low[ x ] > DFN[ y ])

回到点 2,点 2 不能不通过 边 1 到点 1,所以边 1 是割边

主要代码

struct node {
	int to , num ;//to 存到达的节点,num 存走的边的编号
	node () {}
	node ( int To , int Num ) {
		to = To ;
		num = Num ;
	}
};
vector <node> G[1005] ;

void Tarjan( int x , int fnum ) {// fnum 表示到通过边 fnum 到 点 x 
	ber ++ ;//编号,时间戳
	low[x] = DFN[x] = ber ;
	int kid = 0 ;
	for( int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i].to , bn = G[x][i].num ;
		if( !DFN[s] ) {
			kid ++ ;
			Tarjan( s , bn );
			if( low[s] > DFN[x] )
				isC[bn] = 1 ;
			low[x] = min( low[x] , low[s]);
		}
		else if( DFN[x] > DFN[s] && bn != fnum )
				low[x] = min( DFN[s] , low[x] );
	}
}

5. Tarjan算法求边双连通分量

无向图搜索树一棵

求边双,先用Tarjan求出割边 1 , 6 , 7

然后把割边拆掉,剩下的连通块的边就是边双了

实际操作时,标记割边,DFS时不走割边即可

主要代码

struct node {
	int to , num ;//to 存到达的节点,num 存走的边的编号
	node () {}
	node ( int To , int Num ) {
		to = To ;
		num = Num ;
	}
};
vector <node> G[1005] ;

void Tarjan( int x , int fnum ) {// fnum 表示到通过边 fnum 到 点 x 
	ber ++ ;//编号,时间戳
	low[x] = DFN[x] = ber ;
	int kid = 0 ;
	for( int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i].to , bn = G[x][i].num ;
		if( !DFN[s] ) {
			kid ++ ;
			Tarjan( s , bn );
			if( low[s] > DFN[x] )
				isC[bn] = 1 ;
			low[x] = min( low[x] , low[s]);
		}
		else if( DFN[x] > DFN[s] && bn != fnum )
				low[x] = min( DFN[s] , low[x] );
	}
}

void DFS( int x ) {
	vis[x] = 1 ;//访问点x
	D[cnt].push_back( x ) ;//放入边双 
	for( int i = 0 ; i < G[x].size() ; ++ i ) {
		int s = G[x][i].to , bn = G[x][i].num ;
		if( !isC[bn] ) {//不是割边
			if( !vis[s] )//未被访问
				DFS( s );
		}
	}
} 

​

主函数调用DFS

for(int i = 1 ; i <= n ; ++ i ) {
	if( !vis[i] )
		DFS( i );
} 

大概就这样了吧,自己也是刚学不久

猜你喜欢

转载自blog.csdn.net/qq_44013342/article/details/90715209