一.概述
Tarjan 算法是基于DFS的算法,用于求解图的连通性问题。
Tarjan 算法可以在线性时间内求出:
无向图:
- 割点与桥
- 双连通分量
有向图:
- 强连通分量
- 必经点与必经边
1.割点:
若从图中删除节点 x 以及所有与 x 关联的边之后,图将被分成两个或两个以上的不相连的子图,那么称 x 为图的割点。
2.割边/桥:
若从图中删除边 e 之后,图将分裂成两个不相连的子图,那么称 e 为图的桥或割边。
3.搜索树:
在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,我们可以称之为“无向连通图的搜索树”。
4.时间戳:
时间戳是用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序。
用 dfn[x] 来表示。
5.追溯值:
追溯值用来表示从当前节点 x 作为搜索树的根节点出发,能够访问到的所有节点中,时间戳最小的值 —— low[x]。
二.主要思想
主要思想:
通过一次深度优先遍历,即可找出有向图中的强连通分量。
深度优先遍历有两种方式:
- 先访问当前节点,再递归相邻节点。
- 先递归相邻节点,再访问当前节点。
数据结构分析:
- 我们需要给每个节点一个时间戳,这个时间戳代表了该节点被访问的次序。
- 同时,还需要一个记录该节点通过自身/子孙能够回到的最早的时间节点。
实现步骤:
- 我们将所有节点的时间戳初始化为0,构建递归树。
- 循环所有节点,若dfn[i]==0,则递归访问该节点。
- 每次递归访问节点时,我们需要将该节点压栈,给时间戳和回溯值赋初值,同时遍历该节点的相邻节点,如果相邻节点的dfn为0,则其还没有被访问过,递归访问该节点,访问结束时,更新回溯值;如果相邻节点已经在栈中,那么就直接更新回溯值。另一种情况是,该节点已经被访问过,但是从栈中弹出,那么不做任何处理。
- 当遍历完该节点的所有邻接点,我们要判断,该节点的时间戳的回溯值是否相等,若相等,则该节点为强连通分量的根节点。开始弹栈,直到将该节点弹出栈。
三.具体应用
1.求无向图的割边/割点
无向图最最重要的点在于,不能够去处理该节点通向父节点的那条边。
这是有向图和无向图最大的区别。
有向图需要去管在栈里的节点,看能否通过栈里面的节点回到更早的时间,但是为什么要用栈,就是因为一个节点只能访问一次;而对于无向图来说,当前正在访问的节点是通过该节点的父节点访问的,而无向图是不能访问子节点通过父节点的,所以不需要栈。
1)割点的判断方法
- 该点是根节点并且该点有两个及以上的儿子
- 该点不是根节点并且该点有儿子并且该点儿子的追溯值大于或等于该点被访问的时间
//tarjan求无向图的割点
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N=100000;
//链式前向星,用来表示边
struct node{
int to,next;
}edge[N];
int head[N]={-1},cnt=1;//head[i]表示的是以节点i为始点的边,head[i]中的值表示的是第几条边
int time_flag=0,n,m,res=0,root;
int dfn[N]={0},low[N]={0};//dfn时间戳,low追溯值
bool is_articulation[N]={false};//是否是割点
//加边
void add(int u,int v){
edge[cnt].to=v;
edge[cnt].next=head[u];
head[u]=cnt++;
}
void tarjan(int u){
dfn[u]=low[u]=++time_flag;
int child=0;
int v=head[u];
while(v!=-1){
int a=edge[v].to;
if(dfn[a]==0){
child++;
tarjan(a);
low[u]=min(low[u],low[a]);
if((u==root&&child>1)||(u!=root&&low[a]>=dfn[u])){
if(!is_articulation[u]){
is_articulation[u]=true;
res++;
}
}
}
else{
low[u]=min(low[u],dfn[a]);
}
v=edge[v].next;
}
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;//n个顶点,m条边
//构建好无向图,顶点和边的下标都是从1开始
memset(head,-1,sizeof(head));
for(int i=0;i<m;i++){
int x,y;
cin>>x>>y;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
root=i;
tarjan(i);
}
}
cout<<res<<'\n';
for(int i=1;i<=n;i++){
if(is_articulation[i])
cout<<i<<' ';
}
cout<<'\n';
return 0;
}
2)割边的判断方法
若x->y这条边是割边,那么low[y]>dfn[x]
2.求强连通分量(有向图)
//tarjan求强连通分量
#include <iostream>
#include <stack>
using namespace std;
const int N=100;
//链式前向星,用来表示边
struct node{
int to,next;
}edge[N];
int head[N]={-1};//head[i]表示的是以节点i为始点的边,head[i]中的值表示的是第几条边
int time_flag=0,n,m,cnt=0;
int dfn[N]={0},low[N]={0};//dfn时间戳,low追溯值
bool instack[N]={false};
stack<int> s;
//加边
void add(int u,int v){
edge[cnt].to=v;
edge[cnt].next=head[u];
head[u]=++cnt;
}
void tarjan_dfs(int u){
dfn[u]=low[u]=++time_flag;//时间戳和追溯值赋初值
s.push(u);//节点入栈
instack[u]=true;
int v=head[u];
while(v!=-1){//找与u邻接的边
int a=edge[v].to;
if(!dfn[a]){//a还没有被访问过
tarjan_dfs(a);//深度优先访问a节点
low[u]=min(low[a],low[u]);
}
else if(instack[a]){//v已经被访问过
low[u]=min(dfn[a],low[u]);
}
v=edge[v].next;
}
if(low[u]==dfn[u]){//表明节点u是强连通分量的根
int x;
do{
x=s.top();
cout<<x<<' ';
s.pop();
instack[x]={false};
}while(x!=u);
cout<<endl;
}
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;//n个顶点,m条边
//构建好有向图,顶点和边的下标都是从1开始
for(int i=0;i<m;i++){
int x,y;
cin>>x>>y;
add(x, y);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan_dfs(i);
}
}
return 0;
}