1.Tarjan算法求强连通分量
2. Tarjan算法求割点
3. Tarjan算法求点双连通分量
4. Tarjan算法求割边
5. 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 } 为强连通分量
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 ;
}
#include
#include
#include
#include
#include
#include
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 G[1005] ;
stack 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;
}
割点是在无向图的
一棵无向图的搜索树(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] );
}
}
}
了解割点后就直接看点双,一个无向图如下
有点双连通分量 { 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
#include
#include
#include
#include
#include
#include
#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 G[1005] ;
stack S ;
vector 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") ;
}
}
割边就是所谓的桥,其求法与割点相似,先把每一条边标号
一个如下的无向图
割边有边 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 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] );
}
}
无向图搜索树一棵
求边双,先用Tarjan求出割边 1 , 6 , 7
然后把割边拆掉,剩下的连通块的边就是边双了
实际操作时,标记割边,DFS时不走割边即可
struct node {
int to , num ;//to 存到达的节点,num 存走的边的编号
node () {}
node ( int To , int Num ) {
to = To ;
num = Num ;
}
};
vector 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 );
}
大概就这样了吧,自己也是刚学不久