学习笔记-tarjan求强联通分量

强联通分量

  1. 强联通:有向图 \(G\) 强联通表示, \(G\) 中任意两个节点相连通。
  2. 强联通分量 \((Strongly Connected Components ,简称 SCC)\): 极大的强联通子图。

Tarjan

本篇不会直接上来丢给读者 \(dfn[u],low[u]\) ,两个数组,会尽量从头一点一点来写。

  1. 首先思考我们要的是什么
    给定一张有向图,要找出“强联通分量”。
    强联通分量 其实需要满足三个条件:

    • 是有向图 \(G\) 的子图。
    • 这个子图内的所有点是两两互相可达的。
    • 这个子图是极大的

    朴素的想法是:
    对于每一个点做一次 \(DFS\) 看能到谁,再反向看谁能到它。
    \(Tarjan\) 的思路是:
    只用一次 \(DFS\) 在递归回溯的时候把已经成团的强联通分量切下来。

  2. 思路
    在有向图 \(G\) 上做 \(DFS\) ,会有一颗 \(DFS\) 生成树
    考虑以下两种情况:

    • 返祖边 ( 指向祖先 ) : 会把祖先和后代连成环,这个环就是一个 \(SCC\)
    • 横叉边 ( 只想另一颗访问过的子树 ) :如果指向的那个点还没有被划入任意 \(SCC\) 那么也可能和当前节点形成一个 \(SCC\)

    \(Tarjan\) 就是:
    \(DFS\) 的过程中维护一个 \(stack\) 表示当前还在栈里的节点集合,也就是已经 \(DFS\) 到且没有被划入任何 \(SCC\) 的点。
    当发现一个节点满足上述两种情况,就把这个节点当做 \(SCC\) 的一个关键点,把这个点上面所有在栈内的点都弹出,这些点就属于一个 \(SCC\)
    dfs生成树有关知识

  3. 关于 \(dfn[u],low[u]\)
    定义两个数组

    • \(dfn[u]\) 就是节点 \(u\)\(DFS\) 序。

    • \(low[u]\)\(u\) 出发,在 \(DFS\) 树中能追溯到的最早最小\(dfn\) 值。
      注意 \(low[u]\)只包括

      • 自己子树内的节点
      • 通过返祖边或横叉边到仍然在栈中的节点

      不包括

      • 已经被划到其他 \(SCC\) 的节点,因为一个节点不会在两个 \(SCC\) 中。
  4. 实现
    于是我们可以根据上述两个数组来求 \(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
    }
}
posted @ 2026-08-20 23:39  词印  阅读(8)  评论(0)    收藏  举报