Tarjan求强连通分量
前言:
自本文章开始,作者会着重讲述关于\(Tarjan\)的算法问题与题目类型
强连通分量
强连通图指的是在一个有向图中,任意两个点都能相互到达,则称这个图为强连通图
强连通分量指的是一个图中的强连通子图,例如下图:

图中的[3,4,5,6]这4个点构成了一个强连通分量,因为这四点中任意两点都可以相互到达(其实就是一个环)
\(DFS\)生成树

有向图的 DFS 生成树主要有 4 种边(不一定全部出现):
- 树边(tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
- 反祖边(back edge):示意图中以红色边表示,也被叫做回边,即指向祖先结点的边。
- 横叉边(cross edge):示意图中以蓝色边表示,它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
- 前向边(forward edge):示意图中以绿色边表示,它是在搜索的时候遇到子树中的结点的时候形成的。
这里作者直接搬运的oi-wiki上的内容
$ Tarjan $算法
引入
\(Robert E. Tarjan\)(罗伯特·塔扬,1948~),生于美国加州波莫纳,计算机科学家。
算法实现
我们要对图进行一个\(dfs\)搜索,在搜索中我们要维护两个数组\(dfn\)与\(low\)
- \(dfn\)表示这个点在搜索中被搜索的次序,即对这个点进行一个标号,满足一个性质即一个结点的子树内结点的\(dfn\)都大于该结点的 \(dfn\)。
- \(low\)表示这个点所能到达的\(dfn\)值最小的一个点
这里对\(low\)的作用进行一下解释,\(low\)的本质就是一个不断向上跳的过程,因为一个结点的子树内结点的\(dfn\)都大于该结点的 \(dfn\),所以该节点正常来说\(low\)是不会大于\(dfn\)值的,但是在\(dfs\)树中会存在非树边,所以\(low\)值可能会小于该节点的\(dfn\)值,此时我们会发现因为非树边的存在,该图就可能会出现强连通分量,所以我们也可以把整个过程看作求\(dfs\)树中的返祖边(不理解的可以再看看上面的图)
以上就是$ Tarjan $的核心逻辑,下面开始具体实现
按照\(dfs\)搜索的次序对图中所有的结点进行搜索,维护每个结点的 \(dfn\) 与 \(low\) 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。
此时我们还要分析三种情况:
-
当
v未被访问过,即\(dfn\)为空,则访问该结点,并更新\(low_u\),取\(low_u\)与\(low_v\)的最小值 -
当
v被访问过,即已经在栈中,继续更新\(low_u\),取\(low_u\)与\(dfn_v\)的最小值- 这里解释以下为什么是取\(low_u\)与\(dfn_v\)的最小值,而不是取\(low_u\)与\(low_v\)的最小值,因为该点已经被访问所以这条边只会有两种情况,一种是反祖边,一种是横叉边,无论哪种,\(dfn_v\)值必定小于\(dfn_u\),如果取该点的\(low\)值,很可能导致\(low_u\)的值往上跳的太多,不能判断是否构成了环
-
当
v被访问过,已不在栈中,说明该点是强连通中的一个点,不用考虑
下面是示例代码
void Tarjan(ll u) {
deep++;//dfs序
low[u] = dfn[u] = deep;//初始化low与dfn
st.push(u);//将该点压入栈
vis[u]=true;//标记该点,说明已被访问,在栈中
for(auto v:g[u]){
if(!dfn[v]){//对应情况1
Tarjan(v);//先对v点进行搜索,让v点取得最小的low值,在进行更新
low[u]=min(low[v],low[u]);
}
else{
if(vis[v]){//对应情况2,更新low值
low[u]=min(low[u],dfn[v]);
}
}
}
if(low[u]==dfn[u]){//说明这里构成了一个环,出现了强连通分量
SCCcnt++;//强连通分量个数加一
while(1){//开始标记强连通分量中的点
ll t=st.top();//取出栈顶元素
st.pop();
vis[t]=false;//标记该点已经不在栈中
SCCid[t]=SCCcnt;//标记该点所属强连通分量的编号
if(u==t)break;//如果标记到了自己,说明该强连通分量已经标记完毕,结束循环
}
}
}
时间复杂度\(O(n+m)\)
例题:[P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G]([P2341 USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷)
示例代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
ll dfn[N], low[N], vis[N], SCCid[N], out[N];
vector<ll> g[N];
ll deep,SCCcnt;
stack<ll> st;
void Tarjan(ll u) {
deep++;
low[u] = dfn[u] = deep;
st.push(u);
vis[u]=true;
for(auto v:g[u]){
if(!dfn[v]){
Tarjan(v);
low[u]=min(low[v],low[u]);
}
else{
if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
}
if(low[u]==dfn[u]){
SCCcnt++;
while(1){
ll t=st.top();
st.pop();
vis[t]=false;
SCCid[t]=SCCcnt;
if(u==t)break;
}
}
}
int main(){
ll n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
ll u,v;
cin>>u>>v;
g[u].push_back(v);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) Tarjan(i);
}
for(int i=1;i<=n;i++){
for(int v:g[i]){
if(SCCid[i]!=SCCid[v]){
out[SCCid[i]]++;
}
}
}
int zeroOutCnt = 0, ansScc = 0;
for (int i = 1; i <= SCCcnt; i++) {
if (out[i] == 0) {
zeroOutCnt++;
ansScc = i;
}
}
if (zeroOutCnt == 1) {
int ans = 0;
for (int i = 1; i <= n; i++) {
if (SCCid[i] == ansScc) ans++;
}
cout << ans << endl;
} else {
cout << 0 << endl;
}
}

浙公网安备 33010602011771号