005.强连通分量

强连通分量Tarjan算法

USACO06JAN] The Cow Prom S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

强连通:若一张有向图的节点两两可达,则称这张图是强连通的

强连通分量\((SCC)\):极大的强连通子图

Tarjan算法

  1. 时间戳\(dfn[x]\):节点\(x\)第一次被访问的顺序
  2. 追溯值\(low[x]\):从节点\(x\)出发,所能访问到的最早的时间戳

算法流程

  1. 进入节点\(x\)的函数空间:盖戳、入栈
  2. 枚举\(x\)的邻点\(y\),分为三种情况
    • \(y\)尚未访问:对\(y\)进行深度优先搜索(DFS)。从\(y\)返回\(x\)的函数空间时,用\(low[y]\)更新\(low[x]\)。因为\(x\)\(y\)的父节点,\(y\)能访问到的节点,\(x\)也一定能访问到
    • \(y\)已经访问且在栈中:说明\(y\)\(x\)的祖先节点或者左子树节点,\(dfn[y]\)更新\(low[x]\)
    • \(y\)已经访问且不在栈中:说明\(y\)已经搜索完毕,其所在连通分量已被处理,不用进行操作
  3. 离开节点\(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)\)就不是割边了。即环内的边割不断
posted @ 2025-06-16 20:10  _P_D_X  阅读(13)  评论(0)    收藏  举报