Tarjan算法求强连通分量、割点、割边

本篇博客基于OI-Wiki进行了摘录,详细版请点链接

基本介绍

算法应用:求解有向图的强连通分量
Tarjan算法涉及的变量

  • 时间戳 d f n u dfn_{u} dfnu: 深度优先搜索遍历时结点 u u u被搜索的次序
  • 追溯值 l o w u low_{u} lowu: 能够回溯到的最早的已经在栈中的结点。设以 u u u为根的子树为 s u b t r e e u subtree_{u} subtreeu l o w u low_{u} lowu定义为以下结点的 d f n dfn dfn的最小值: s u b t r e e u subtree_{u} subtreeu中的结点;从 s u b t r e e u subtree_{u} subtreeu 通过一条不在搜索树上的边能到达的结点。

一个结点子树内结点的 d f n dfn dfn都大于该结点 d f n dfn dfn
从根开始的一条路径上的 d f n dfn dfn严格递增, l o w low low严格非降
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索。在搜索过程中,对于结点 u u u和与其相邻的结点 v v v即边<u,v> v v v不是 u u u的父节点)考虑 3 种情况:

  • v v v未被访问:继续对 v v v进行深度搜索。在回溯过程中,用 l o w v low_{v} lowv更新 l o w u low_{u} lowu l o w u = m i n ( l o w u , l o w v ) low_{u}=min(low_{u},low_{v}) lowu=min(lowu,lowv)。因为存在从 u u u v v v的直接路径,所以 v v v能够回溯到的已经在栈中的结点, u u u也一定能够回溯到。
  • v v v被访问过,已经在栈中:根据 low 值的定义,用 d f n v dfn_{v} dfnv更新 l o w u low_{u} lowu, l o w u = m i n ( l o w u , d f n v ) low_{u}=min(low_{u},dfn_{v}) lowu=min(lowu,dfnv)
  • v v v被访问过,已不在栈中,说明 v v v已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

求强连通分量

对于一个连通分量图,在该连通图中有且只有一个u使得 d f n u = l o w u dfn_{u} = low_{u} dfnu=lowu
该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 dfn 和 low 值最小,不会被该连通分量中的其他结点所影响
因此,在回溯的过程中,判定 d f n u = l o w u dfn_{u}=low_{u} dfnu=lowu是否成立,如果成立,则栈中 u u u及其上方的结点构成一个 SCC

SCC:Strongly Connected Components 强连通分量

// OI-Wiki板子基础上进行修改,用了点容器
#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define fast ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define de(x) cout <<':' << x << endl

const int N = 1e5+5;

int dfn[N], low[N], dfncnt;
bool in_stack[N];
int scc[N], sc;//结点i所在的SCC的编号
int sz[N];     //强连通分量i的大小

stack<int> s;
vector<int> g[N];

void init()
{
    dfncnt = sc = 0;
    for(int i=0;i<N;++i)
    {
        dfn[i] = low[i] = 0;
        in_stack[i] = false;
        scc[i] = sz[i] = 0;
    }
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++dfncnt;
    s.push(u);
    in_stack[u] = true;
    for(int i=0;i<g[u].size();++i)
    {
        int v = g[u][i];
        if(!dfn[v])//v未被访问,继续对v进行深度搜索
        {
            tarjan(v);
            low[u] = min(low[u],low[v]);
        }
        else
        if(in_stack[v])//被访问过已在栈中,用dfnv更新lowu
        {
            low[u] = min(low[u],dfn[v]);
        }//其余情况说明v已搜索完毕,不用操作
    }

    if(dfn[u]==low[u])//一个强连通分量被发现
    {
        ++sc;
        while(s.top()!=u)
        {
            scc[s.top()] = sc;
            sz[sc]++;
            in_stack[s.top()] = false;
            s.pop();
        }
        scc[s.top()] = sc;
        sz[sc]++;
        in_stack[s.top()] = false;
        s.pop();
    }
}

int main()
{
    
    return 0;
}

求割点、割边

u是割点的条件

  • v是u搜索树上的一个儿子, d f n u < = l o w v dfn_{u}<=low_{v} dfnu<=lowv,即v的子树中没有返祖边能跨越u点
  • 有多个儿子

边是桥的条件

  • 搜索树上v是u的儿子, d f n u < l o w v dfn_{u}<low_{v} dfnu<lowv,即v的子树中没有返祖边能跨越<u,v>这条边

算法应用

NC15707 连通性

题目链接
使用tarjan算法进行缩点操作,将每一个强连通分量视作一个点,找出所有入度为0的点即可。
问:为什么不能直接找入度为0的点?
答:可能存在情况,所有点入度都不为0,此时无法得到正确答案。
代码:

#include<bits/stdc++.h>

using namespace std;

#define ll long long
#define fast ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define de(x) cout <<':' << x << endl

const int N = 1e5+5;

int dfn[N], low[N], dfncnt;
bool in_stack[N], in[N];
int scc[N], sc;//结点i所在的SCC的编号
int sz[N];     //强连通分量i的大小

stack<int> s;
vector<int> g[N];

void init()
{
    dfncnt = sc = 0;
    for(int i=0;i<N;++i)
    {
        dfn[i] = low[i] = 0;
        in_stack[i] = in[i] = false;
        scc[i] = sz[i] = 0;
    }
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++dfncnt;
    s.push(u);
    in_stack[u] = true;
    for(int i=0;i<g[u].size();++i)
    {
        int v = g[u][i];
        if(!dfn[v])//v未被访问,继续对v进行深度搜索
        {
            tarjan(v);
            low[u] = min(low[u],low[v]);
        }
        else
        if(in_stack[v])//被访问过已在栈中,用dfnv更新lowu
        {
            low[u] = min(low[u],dfn[v]);
        }//其余情况说明v已搜索完毕,不用操作
    }

    if(dfn[u]==low[u])//一个强连通分量被发现
    {
        ++sc;
        while(s.top()!=u)
        {
            scc[s.top()] = sc;
            sz[sc]++;
            in_stack[s.top()] = false;
            s.pop();
        }
        scc[s.top()] = sc;
        sz[sc]++;
        in_stack[s.top()] = false;
        s.pop();
    }
}

int main()
{
    init();
    int n, m;
    scanf("%d %d",&n,&m);
    for(int i=0;i<m;++i)
    {
        int u, v;
        scanf("%d %d",&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 j=0;j<g[i].size();++j)
        {
            int v = g[i][j];
            if(scc[i]!=scc[v])
            {
                in[scc[v]] = true;
            }
        }
    }
    vector<int> ans;
    for(int i=1;i<=n;++i)
    {
        if(!in[scc[i]])
        {
            ans.push_back(i);
            in[scc[i]] = true;
        }
    }
    printf("%d\n",ans.size());
    for(int i=0;i<ans.size();++i)
    {
        printf("%d ",ans[i]);
    }
    printf("\n");
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值