浅谈用Tarjan算法求解SCC(强连通分量)问题

开篇废话

关于本蒟蒻五一假期一直在研究tarjan算法,感觉弄懂了,发篇博客庆祝一下。

前置知识(懂的可以跳过)

都已经发两篇博客了,还没讲一下什么是图呢。

以下内容来自百度百科:

图论〔Graph Theory〕是数学的一个分支。它以为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。

图G=(V,E)是一个二元组(V,E)使得E⊆[V]的平方,所以E的元素是V的2-元子集。

说人话就是,图是一个数学里研究的东西,数学里不是数,就是图,图由点和边构成且点数边数都是正整数。

联通

联通图:百度给的解释太复杂(我也没看懂),简单来说,在一个无向图里,任意两个点都可以相互到达,则称这个图是联通的。

 强连通图:强连通是指一个有向图任意两点v1、v2间存在v1到v2的路径及v2到v1的路径。一个点也是一个强连通图。

弱连通分量(不常考):如果一个非强连通分量图变成无向图后是一个连通图,则这个图是一个弱连通分量图。

强连通分量

定义

不是所有有向图都是强连通图,但一定存在这个图的子图中一定有强连通图,则这个字图就是原图的一个强连通分量。强连通图的强连通分量只有一个,任意有向图的强连通分量最多有点数那么多个。

作用

我们知道,在图论里,最让人厌烦的一样东西莫过于环,最短路的克星就是负环,最小生成树也是要生成一个没有环的图,在编程中处理环是很费劲的,所以SCC(强连通分量)就有用武之地了。

缩点

众所周知,强连通分量里的点都是彼此相互连通的(定义告诉我们的),所以,只要其中一个点具有某种性质(能被遍历……),该强连通分量里的所有点都一定能具有这一性质,所以,一个强连通分量和一个点是没有区别的,我们就可以将一个有向图看作很多个点组成的拓扑图(没有环的图),对这个图的操作就容易多了。

Tarjan

概述

tarjan算法的本质就是深搜,在深搜中维护某些值。

算法思路

我们首先引入一个叫做时间戳的概念,不要没这么高大尚的名字吓到,它的本质就是在这个图中第几个被访问的点。我们需要两个数组,一个叫dfn,一个叫low,分别表示这个点的时间戳和所有它能到达的点的时间戳的最小值。

我们从一个点开始搜索,把这个点压入栈中,修改low和dfn的值为时间戳。

从这个点开始遍历,如果他的dfn还是0,说明他还没被遍历过,就在tarjan这个点,用他的low值来更新自己的low值。

如果这个点还在队列里,就用他的dfn值来更新自己的low值

循环结束后,只要这个点的dfn值等于low值,说明这个点是一个强连通分量的起点,那就不断弹出栈顶元素,直到弹出的元素就是这个点,更新维护强连通分量的数组。

过程模拟

 

 

 

 

这样,所有点都被遍历过,开始深搜回溯

 5的low值和dfn值相等,本身就是一个强连通分量

4还有一条连边指向2,2被遍历过,所以用2的dfn值来更新4的low值

回溯到3,用4来更新3

 2能走到1,用1的dfn值来更新2

 1没有边可以遍历了,dfn值等于low值

强连通分量全部被找到了

代码

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;//赋值
    stk[ ++ top] = u, in_stk[u] = true;//频繁操作的时候习惯用手写栈
    for (int i = h[u]; i != -1; i = ne[i])//用链式前向星存图
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);//深搜
            low[u] = min(low[u], low[j]);//更新
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);//还在栈里就更新
    }
 
    if (dfn[u] == low[u])//判断是不是一个强连通分量
    {
        ++ scc_cnt;//强连通分量计数器
        int y;
        do {
            y = stk[top -- ];//出栈
            in_stk[y] = false;
            id[y] = scc_cnt;//保存结果
            Size[scc_cnt] ++ ;
        } while (y != u);
    }
}

例题

受欢迎的牛

[题目描述]

每一头牛的愿望就是变成一头最受欢迎的牛。现在有N头牛,给你M对整数(A,B),表示牛A认为牛B受欢迎。 这种关系是具有传递性的,如果A认为B受欢迎,B认为C受欢迎,那么牛A也认为牛C受欢迎。你的任务是求出有多少头牛被所有的牛认为是受欢迎的。

输入

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

输出

一个数,即有多少头牛被所有的牛认为是受欢迎的。

样例输入

3 3
1 2
2 1
2 3

样例输出

1

提示

10%的数据N<=20, M<=50

30%的数据N<=1000,M<=20000

70%的数据N<=5000,M<=50000

100%的数据N<=10000,M<=50000

思路分析

1.缩点,求强连通分量

2.统计拓扑图内每一个点(缩点后的点)的出度

3.用计数器记录出度不为零的点(强连通分量)的元素个数

4.如果有两个及以上的点出度等于零,计数器归零,跳出循环

AC代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
 
using namespace std;
 
const int N = 10010, M = 50010;
 
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, Size[N];
int dout[N];
 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
 
void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u, in_stk[u] = true;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
    }
 
    if (dfn[u] == low[u])
    {
        ++ scc_cnt;
        int y;
        do {
            y = stk[top -- ];
            in_stk[y] = false;
            id[y] = scc_cnt;
            Size[scc_cnt] ++ ;
        } while (y != u);
    }
}
 
int main()
{
    scanf("%d%d", &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])
            tarjan(i);
 
    for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if (a != b) dout[a] ++ ;
        }
 
    int zeros = 0, sum = 0;
    for (int i = 1; i <= scc_cnt; i ++ )
        if (!dout[i])
        {
            zeros ++ ;
            sum += Size[i];
            if (zeros > 1)
            {
                sum = 0;
                break;
            }
        }
 
    printf("%d\n", sum);
 
    return 0;
}

 网络协议/学校网络

[题目描述]

一些学校连接在一个计算机网络上。学校之间存在软件支援协议。每个学校都有它应支援的学校名单(学校 a 支援学校 b,并不表示学校 b 一定支援学校 a)。当某校获得一个新软件时,无论是直接得到还是网络得到,该校都应立即将这个软件通过网络传送给它应支援的学校。因此,一个新软件若想让所有连接在网络上的学校都能使用,只需将其提供给一些学校即可。

任务

请编一个程序,根据学校间支援协议(各个学校的支援名单),计算最少需要将一个新软件直接提供给多少个学校,才能使软件通过网络被传送到所有学校;

如果允许在原有支援协议上添加新的支援关系。则总可以形成一个新的协议,使得此时只需将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件。编程计算最少需要添加几条新的支援关系。

输入

第一行是一个正整数 n,表示与网络连接的学校总数。 随后 n 行分别表示每个学校要支援的学校,即:i+1 行表示第 i 号学校要支援的所有学校代号,最后 0 结束。

如果一个学校不支援任何其他学校,相应行则会有一个 0。一行中若有多个数字,数字之间以一个空格分隔。

输出

包含两行,第一行是一个正整数,表示任务 a 的解,第二行也是一个正整数,表示任务 b 的解。

样例输入

5
2 4 3 0
4 5 0
0
0
1 0

样例输出

1
2

提示

2≤n≤100

思路分析

1.缩点,求强连通分量

2.统计拓扑图内每一个点(缩点后的点)的出度和入度

3.输出的第一行即为入度不为零的个数

4.如果只有一个强连通分量,第二行输出0

5.输出的第二行即为出度入度的最大值

AC代码

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110, M = 10010;

int n;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];

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

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u, in_stk[u] = true;

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }

    if (dfn[u] == low[u])
    {
        ++ scc_cnt;
        int y;
        do {
            y = stk[top -- ];
            in_stk[y] = false;
            id[y] = scc_cnt;
        } while (y != u);
    }
}

int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ )
    {
        int t;
        while (cin >> t, t) add(i, t);
    }

    for (int i = 1; i <= n; i ++ )
        if (!dfn[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];
            if (a != b)
            {
                dout[a] ++ ;
                din[b] ++ ;
            }
        }

    int a = 0, b = 0;
    for (int i = 1; i <= scc_cnt; i ++ )
    {
        if (!din[i]) a ++ ;
        if (!dout[i]) b ++ ;
    }

    printf("%d\n", a);
    if (scc_cnt == 1) puts("0");
    else printf("%d\n", max(a, b));

    return 0;
}

这是我的第三篇文章,如有纰漏也请各位大佬指正

辛苦创作不易,还望看官点赞收藏打赏,后续还会更新新的内容。

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值