有向图强连通分量tarjan算法详解(适合新手) + 模板题:《信息学奥赛一本通》 , USACO , HAOI2006 受欢迎的牛

先了解几个概念:

有向图联通分量:对于图中任意两点 u, v 都可以从u走到v也可以从 v 走到 u。

强连通分量:极大联通分量。

tarjan算法求强连通分量:时间复杂度O(n + m);

缩点:将两个两两能到的点压缩为一个联通分量即可以看出一个点。

即将图:

 缩点之后为:

 tarjan算法就是找出所有的这样的联通分量。

注意一个性质:联通分量递减的顺序一定是拓扑图(因为tarjan算法是用dfs遍历的,即先遍历的点后输出)

然后我们介绍一下tarjan算法的几个变量:

时间戳timestamp; 遍历的点的顺序

dfn[i] : 表示点 i 的时间戳是多少。

low[i] : 表示点 i 能到达的的最小时间戳

scc_cnt :表示连通分量的数目

id[i] : 表示点 i 在哪个联通分量中

Size[scc_cnt] : 表示scc_cnt这个联通分量中缩点之前有多少个点。

栈stk :存的是当前遍历的联通分量中的点。

tarjan算法模板如下:

void tarjan(int u)//u为当前正在遍历哪个点
{
    dfn[u] = low[u] = ++ timestamp;//将时间戳赋值给dfs[u](定义)。赋值给low[u]是为了后续判断
    stk.push(u), in_stk[u] = true;//将点 u 压入栈中
    
    for (int i = h[u]; i != -1; i = ne[i])//枚举 u 的每条边
    {
        int j = e[i];
        if (!dfn[j])//若 j 还未被遍历。
        {
            tarjan(j);//遍历 j 。
            low[u] = min(low[u], low[j]);//因为 u 可以走到 j 所以 j 能走到的时间戳最小的点 u 也能走到。
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
    }           //如果 j 在栈中,说明dfn[j]直接已经被赋值,因为时间戳是从小到大遍历的.
                //切此时在遍历u , j所在的连通块,所以可以用dfn[j]来更新low[u];
    if (low[u] == dfn[u])//说明遍历到了联通分量的最后一个点
    {
        int y;
        scc_cnt ++ ;
        
        do
        {
            y = stk.top();//将栈中的联通分量的点都取出来
            stk.pop();//删去
            
            id[y] = scc_cnt;//记录点y在哪个联通分量
            Size[scc_cnt] ++ ;//联通分量scc_cnt里面的点数 ++ 。
        } while (y != u);//若y == u说明联通块中已经全部取出
    }
}

 例题如下:

每一头牛的愿望就是变成一头最受欢迎的牛。

现在有 N 头牛,编号从 1 到 N,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。

这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。

你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

输入格式

第一行两个数 N,M;

接下来 M 行,每行两个数 A,B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出现多个 A,B)。

输出格式

输出被除自己之外的所有牛认为是受欢迎的牛的数量。

数据范围

1≤N≤10^4,
1≤M≤5×10^4

输入样例:

3 3
1 2
2 1
2 3

输出样例:

1

样例解释

只有第三头牛被除自己之外的所有牛认为是受欢迎的。

解题思路。将两两吸引的牛缩点为一个拓扑图。

缩点之后的图若是有两个出度为0的联通分量则说明无解:

如图

点1和点2永远不可能被所有点遍历到,因为点1不能遍历到点二,点二不能遍历到点一

两个及以上情况相同、

若缩点之后只有一个出度为0的联通分量。即如图:

如图点1,2,3,4所在的联通分量的出入为0,则点1,2,3,4都可以被其他点遍历到

即sum += Size[点1,2,3,4所在的联通分量] 

#include <stack>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 10010, M = 50010;

int n, m;
int id[N];
int dout[N];
int Size[N];
int scc_cnt;
int timestamp;
stack<int> stk;
bool in_stk[N];
int dfn[N], low[N];
int h[N], e[M], ne[M], idx;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx;
    idx ++ ;
}

void tarjan(int u)//u为当前正在遍历哪个点
{
    dfn[u] = low[u] = ++ timestamp;//将时间戳赋值给dfs[u](定义)。赋值给low[u]是为了后续判断
    stk.push(u), in_stk[u] = true;//将点 u 压入栈中
    
    for (int i = h[u]; i != -1; i = ne[i])//枚举 u 的每条边
    {
        int j = e[i];
        if (!dfn[j])//若 j 还未被遍历。
        {
            tarjan(j);//遍历 j 。
            low[u] = min(low[u], low[j]);//因为 u 可以走到 j 所以 j 能走到的时间戳最小的点 u 也能走到。
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
    }           //如果 j 在栈中,说明dfn[j]直接已经被赋值,因为时间戳是从小到大遍历的.
                //切此时在遍历u , j所在的连通块,所以可以用dfn[j]来更新low[u];
    if (low[u] == dfn[u])//说明遍历到了联通分量的最后一个点
    {
        int y;
        scc_cnt ++ ;
        
        do
        {
            y = stk.top();//将栈中的联通分量的点都取出来
            stk.pop();//删去
            
            id[y] = scc_cnt;//记录点y在哪个联通分量
            Size[scc_cnt] ++ ;//联通分量scc_cnt里面的点数 ++ 。
        } while (y != u);//若y == u说明联通块中已经全部取出
    }
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b);
    }
    
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i])//若点 i 没被遍历
            tarjan(i);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; j != -1; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];//找到点a, b所在的联通块
            
            if (a != b) dout[a] ++ ;//说明他们不在一个连通块中因为是a -> b所以a所在的联通块的出度++
        }
    
    int sum = 0, cnt = 0;    
    for (int i = 1; i <= scc_cnt; i ++ )//枚举每个连通块
        if (!dout[i])//若出度为0
        {
            cnt ++ ;
            sum += Size[i];//加上连通块 i 中的点数
            
            if (cnt > 1)//若有两个联通块出度为0
            {
                sum = 0;
                break;
            }
        }
    
    cout << sum << endl;
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啥也不会hh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值