005.强连通分量
强连通分量Tarjan算法
USACO06JAN] The Cow Prom S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
强连通:若一张有向图的节点两两可达,则称这张图是强连通的
强连通分量\((SCC)\):极大的强连通子图
Tarjan算法
- 时间戳\(dfn[x]\):节点\(x\)第一次被访问的顺序
- 追溯值\(low[x]\):从节点\(x\)出发,所能访问到的最早的时间戳
算法流程
- 进入节点\(x\)的函数空间:盖戳、入栈
- 枚举\(x\)的邻点\(y\),分为三种情况
- 若\(y\)尚未访问:对\(y\)进行深度优先搜索(DFS)。从\(y\)返回\(x\)的函数空间时,用\(low[y]\)更新\(low[x]\)。因为\(x\)是\(y\)的父节点,\(y\)能访问到的节点,\(x\)也一定能访问到
- 若\(y\)已经访问且在栈中:说明\(y\)是\(x\)的祖先节点或者左子树节点,用\(dfn[y]\)更新\(low[x]\)
- 若\(y\)已经访问且不在栈中:说明\(y\)已经搜索完毕,其所在连通分量已被处理,不用进行操作
- 离开节点\(x\)的函数空间,返回上一层时:记录\(SCC\),只有遍历完一个\(SCC\),才可以出栈
更新\(low\)值的意义:避免\(SCC\)的节点提前出栈
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>·
using namespace std;
const int N=10010;
int n,m,a,b;
//存图:邻接表
vector<int> e[N];
//时间戳dfn,追溯值low,tot控制遍历顺序
int dfn[N],low[N],tot;
//手写栈
int stk[N],instk[N],top;
//记录连通分量
int scc[N],siz[N],cnt;
//Tarjan算法
void dfs(int x){
//进入x时:盖戳、入栈
dfn[x] = low[x] = ++tot;
stk[++top] = x,instk[x] = 1;
//遍历所有出边
for(int y : e[x]){
//若y尚未被访问
if(!dfn[y]){
dfs(y);
//返回x时更新low
low[x] = min(low[x],low[y]);
}
//若y已被访问且在栈中
else if(instk[y])
//直接更新low
low[x] = min(low[x],dfn[y]);
}
//离开x时:记录scc
//如果x是scc的根
if(dfn[x] == low[x]){
int y; ++ cnt;
do{
y = stk[top--]; instk[y] = 0;
scc[y] = cnt;//scc编号
++siz[cnt];//scc大小
}while(y != x);
}
}
int main(){
cin>>n>>m;
while(m--)
cin>>a>>b, e[a].push_back(b);
for(int i=1; i<=n; i++)//可能不连通
if(!dfn[i]) dfs(i);
int ans=0;
for(int i=1;i<=cnt;i++)
if(siz[i]>1) ans++;
cout<<ans<<endl;
return 0;
}
时间复杂度:\(O(n+m)\)
Tarjan 割点
P3388 【模板】割点(割顶) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
割点:对于一个无向图,如果把一个点删除后,连通块的个数增加了,那么这个点就是割点
割点判定法则
- 如果\(x\)不是根节点,\(\exists y(y\)是\(x\)的子节点\(\land low[y] \ge dfn[x])\rightarrow x\)是割点;
- 如果\(x\)是根节点,\(\exists y_1,y_2(y_1,y_2\)是\(x\)的子节点\(\land low[y_1] \ge dfn[x] \land low[y_2] \ge dfn[x])\rightarrow x\)是割点;
正确性证明
- 若\(low[y] \ge dfn[x]\),说明从\(y\)出发,在不通过\(x\)点的前提下,不管走哪条边,都无法到达比\(x\)更早访问的节点。故删除\(x\)点后,以\(y\)为根的子树\(subtree(y)\)也就断开了。即环顶的点割得掉
- 若\(low[y] < dfn[x]\),说明\(y\)能绕行其他边到达比\(x\)更早访问的节点,\(x\)就不是割点了。即环内的点割不掉
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N=20010;
int n,m,a,b;
//存图:邻接表
vector<int> e[N];
//时间戳dfn,追溯值low,tot控制遍历顺序
int dfn[N],low[N],tot;
//记录割点cut,记录根root
int cut[N],root;
//Tarjan算法
void dfs(int x){
//进入x时:盖戳、入栈
dfn[x] = low[x] = ++tot;
int child = 0;
for(int y : e[x]){
//若y尚未被访问
if(!dfn[y]){
dfs(y);
//返回x时:更新low,判断割点
low[x] = min(low[x],low[y]);
if(low[y] >= dfn[x]){
child++;//子树个数
if(x!=root || child > 1)
cut[x] = true;
}
}
//若y已经被访问过
else
low[x] = min(low[x],dfn[y]);
}
}
int main(){
cin>>n>>m;
while(m --){
cin>>a>>b;
e[a].push_back(b),
e[b].push_back(a);
}
for(root=1; root<=n; root++)
if(!dfn[root]) dfs(root);
int ans=0;
for(int i=1;i<=n;i++)
if(cut[i]) ans++;
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(cut[i]) printf("%d ",i);
return 0;
}
时间复杂度:\(O(n+m)\)
Tarjan 割边
割边:对于一个无向图,如果删掉一条边后图中的连通块个数增加了,则称这条边为桥或者割边
割边判定法则
- 当搜索树上\(\exists y(y\)是\(x\)的子节点\(\land low[y]>dfn[x])\rightarrow (x,y)\)是割边
正确性证明
- 若\(low[y]>dfn[x]\),说明从\(y\)出发,在不经过\((x,y)\)这条边的前提下,不管走哪条边,都无法到达\(x\)或更早访问的节点。故删除\((x,y)\)这条边,以\(y\)为根的子树\(subtree(y)\)也就断开了。即环外的边割得断
- 若\(low[y] \le dfn[x]\),则说明\(y\)能绕行其他边到达\(x\)或更早访问的节点,\((x,y)\)就不是割边了。即环内的边割不断

浙公网安备 33010602011771号