学习笔记-tarjan求强联通分量
强联通分量
- 强联通:有向图 \(G\) 强联通表示, \(G\) 中任意两个节点相连通。
- 强联通分量 \((Strongly Connected Components ,简称 SCC)\): 极大的强联通子图。
Tarjan
本篇不会直接上来丢给读者 \(dfn[u],low[u]\) ,两个数组,会尽量从头一点一点来写。
-
首先思考我们要的是什么
给定一张有向图,要找出“强联通分量”。
强联通分量 其实需要满足三个条件:- 是有向图 \(G\) 的子图。
- 这个子图内的所有点是两两互相可达的。
- 这个子图是极大的
朴素的想法是:
对于每一个点做一次 \(DFS\) 看能到谁,再反向看谁能到它。
\(Tarjan\) 的思路是:
只用一次 \(DFS\) 在递归回溯的时候把已经成团的强联通分量切下来。 -
思路
在有向图 \(G\) 上做 \(DFS\) ,会有一颗 \(DFS\) 生成树
考虑以下两种情况:- 返祖边 ( 指向祖先 ) : 会把祖先和后代连成环,这个环就是一个 \(SCC\) 。
- 横叉边 ( 只想另一颗访问过的子树 ) :如果指向的那个点还没有被划入任意 \(SCC\) 那么也可能和当前节点形成一个 \(SCC\) 。
\(Tarjan\) 就是:
在 \(DFS\) 的过程中维护一个 \(stack\) 表示当前还在栈里的节点集合,也就是已经 \(DFS\) 到且没有被划入任何 \(SCC\) 的点。
当发现一个节点满足上述两种情况,就把这个节点当做 \(SCC\) 的一个关键点,把这个点上面所有在栈内的点都弹出,这些点就属于一个 \(SCC\) 。
dfs生成树有关知识 -
关于 \(dfn[u],low[u]\)
定义两个数组-
\(dfn[u]\) 就是节点 \(u\) 的 \(DFS\) 序。
-
\(low[u]\) 从 \(u\) 出发,在 \(DFS\) 树中能追溯到的最早最小的 \(dfn\) 值。
注意 \(low[u]\)只包括- 自己子树内的节点
- 通过返祖边或横叉边到仍然在栈中的节点
不包括
- 已经被划到其他 \(SCC\) 的节点,因为一个节点不会在两个 \(SCC\) 中。
-
-
实现
于是我们可以根据上述两个数组来求 \(SCC\) ,具体就是你 \(DFS\) 下去,进入这个点的时候令dfn[u]=low[u]=++tim;并压入栈,然后按照两种情况往下递归,在回溯到这个点的时候如果仍然dfn[u]=low[u]那么这个点就是我们所谓的一个 \(SCC\) 的关键点,栈中从这个点到栈顶就是一个 \(SCC\) 。
例题与代码
以一道例题来讲解代码以及具体的 \(Tarjan\) 的做法
题目连接
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
题目简述
给一张 \(N\) 个点 \(M\) 条边的有向图。如果一个点能从这张图的所有其他点走有向边到达,那么就算作一个合法点,统计这张图中有多少合法点
题解
首先我们发现一个 \(SCC\) 中的点都互相可达,于是我们先找所有的 \(SCC\) 缩点,形成一个 \(DAG\) 考虑这个 \(DAG\) 只有一个 \(SCC\) 的出度为 \(0\) 的话,那么答案就是这个出度为 \(0\) 的 \(SCC\) 的大小。
但是如果出度为 \(0\) 的 \(SCC\) 不止一个,那么这些出度为 \(0\) 的 \(SCC\) 之间互相不可达,那么这张图的合法点数目 \(=0\)。
code
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 1e4+10;
const int M = 5e4+10;
int n,m;
vector<int> e[N];
stack<int> st;
int dfn[N],low[N],tim=0;
bool vis[N];
int scc_cnt=0;
vector<int> scc[N];
int idscc[N];
int outdeg[N],ocnt=0,ans=0;
void tarjan(int u){
low[u]=dfn[u]=++tim;
vis[u]=true;
st.push(u);
for(auto v : e[u]){
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
scc[++scc_cnt].pb(u);
vis[u]=false;
idscc[u]=scc_cnt;
while(st.top()!=u){
scc[scc_cnt].pb(st.top());
vis[st.top()]=false;
idscc[st.top()]=scc_cnt;
st.pop();
}
st.pop();
}
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>m;
memset(low,0x3f,sizeof(low));
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
e[u].pb(v);
}
for(int i=1;i<=n;i++){
if(!dfn[i]) tarjan(i);
}
for(int u=1;u<=n;u++){
for(auto v : e[u]){
if(idscc[u]!=idscc[v]){
outdeg[idscc[u]]++;
}
}
}
for(int i=1;i<=scc_cnt;i++){
if(outdeg[i]==0){
ans+=scc[i].size();
ocnt++;
}
}
cout<<((ocnt==1) ? ans : 0);
return 0;
}
Tarjan 部分实现详解
idscc是指每一个点对应的是哪一个scc
scc是一个vector,把所有当前scc的点存下
void tarjan(int u){
low[u]=dfn[u]=++tim; // 初始化时间戳
vis[u]=true; // 标记入栈
st.push(u); // 入栈
for(auto v : e[u]){
if(!dfn[v]){ // 未访问 → 树边
tarjan(v); // 继续递归
low[u]=min(low[u],low[v]);
}else if(vis[v]){ // 已访问且在栈中 → 返祖/横叉边
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){ // 是SCC关键点
scc[++scc_cnt].pb(u); // 新建SCC
vis[u]=false;
idscc[u]=scc_cnt;
while(st.top()!=u){ // 弹出直到遇到u
scc[scc_cnt].pb(st.top());
vis[st.top()]=false;
idscc[st.top()]=scc_cnt;
st.pop();
}
st.pop(); // 弹出u
}
}

浙公网安备 33010602011771号