【图论】有向图的强连通分量

算法提高课笔记(本篇未更新完 还有俩例题)

文章目录

理论基础

什么是连通分量?

对于一个有向图,分量中任意两点u,v,必然可以从u走到v,且从v走到u,这样的分量叫做连通分量

如果一个连通分量加上任意一个点都不是连通分量了,就把它叫做 强连通分量

强连通分量的主要作用:将任意一个有向图转化成一个有向无环图即拓扑图(通过缩点的方式),缩点就是将所有连通分量缩成一个点

如何求强连通分量呢?

按照DFS的顺序搜,我们可以将边分为以下四类:

  1. 树枝边:(x, y),x是y的父结点
  2. 前向边:(x, y),x是y的祖先结点
  3. 后向边:(x, y),y是x的祖先结点
  4. 横叉边:往之前搜过的其他点搜

    怎么判断一个点是否在强连通分量中?
  • 情况一:存在后向边指向祖先结点
  • 情况二:先走到横叉边,横叉边再走到祖先结点

(反正一定可以走到某个祖先)

基于这个想法------ Tarjon 算法求强连通分量(SCC)

先给每个结点按照 DFS 访问顺序确定一个时间戳,时间戳越小说明越先访问到

dfn[u]:遍历到 u 的时间戳
low[u]:从 u 开始走,能遍历到的最小时间戳
id[i]:i 所在连通分量的编号

u是其所在强连通分量的最高点 <-> dfb[u] == low[u]

SCC板子

cpp 复制代码
void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp; // 先将dfn和low都初始化为时间戳
    stk.push(u), in_stk[u] = true; // u加入栈中

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i]; // 取出u的所有邻点j
        if (!dfn[j]) // 如果j还没被遍历
        {
            tarjan(j);
            low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]); // 如果j已入栈 则用dfn[j]更新low[u]
    }

    if (dfn[u] == low[u]) // 如果该点是所在强连通分量的最高点
    {
        ++ scc_cnt; // 强连通分量数量加一
        int y;
        do {
            y = stk.top(); // 取出栈顶元素
            stk.pop();
            in_stk[y] = false;

            id[y] = scc_cnt; // 标记每个点所在的连通分量编号
        } while (y != u); // 直到取到此连通分量的最高点为止
    }
}

缩点的步骤:

  1. 遍历所有点 i
  2. 遍历 i 的所有邻点 j
  3. 如果 i 和 j 不在同一个连通分量中,就加一条新边 id[i]->id[j]
cpp 复制代码
for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j]; // 遍历i的所有邻点k
            int a = id[i], b = id[k]; // 记录ik所在连通分量编号
            if (a != b) dout[a] ++ ; // 如果ik不在同一个连通分量 就在两个连通分量之间连一条i指向k的边
        }

做完tarjon后,连通分量编号递减的顺序一定就是拓扑序

例题

受欢迎的牛

原题链接

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

现在有 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 ≤ 104 , 1≤N≤104, 1≤N≤104,
1 ≤ M ≤ 5 × 104 1≤M≤5×104 1≤M≤5×104

输入样例

cpp 复制代码
3 3
1 2
2 1
2 3

输出样例

cpp 复制代码
1

样例解释

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

题意

一头牛会欢迎另一头牛,这种欢迎是有传递性的,现给出多对欢迎关系,问有几头牛是被其余所有牛欢迎的

思路

先对连通分量进行缩点操作

之后分析,如果有唯一一个连通分量满足出度为0,那么这个连通分量内的所有点都可以由其余任意点到达,答案就是这个连通分量内点的个数,如果这样的连通分量超过一个就不满足条件

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

const int  N = 10010, M = 50010;

int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
stack<int> stk;
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; // 先将dfn和low都初始化为时间戳
    stk.push(u), in_stk[u] = true; // u加入栈中

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i]; // 取出u的所有邻点j
        if (!dfn[j]) // 如果j还没被遍历
        {
            tarjan(j);
            low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]); // 如果j已入栈 则用dfn[j]更新low[u]
    }

    if (dfn[u] == low[u]) // 如果该点是所在强连通分量的最高点
    {
        ++ scc_cnt; // 强连通分量数量加一
        int y;
        do {
            y = stk.top(); // 取出栈顶元素
            stk.pop();
            in_stk[y] = false;

            id[y] = scc_cnt; // 标记每个点所在的连通分量编号
            Size[scc_cnt] ++ ; // 更新此连通分量中的点个数
        } while (y != u); // 直到取到此连通分量的最高点为止
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;
    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b;
        cin >> 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]; // 遍历i的所有邻点k
            int a = id[i], b = id[k]; // 记录ik所在连通分量编号
            if (a != b) dout[a] ++ ; // 如果ik不在同一个连通分量 就在两个连通分量之间连一条i指向k的边
        }

    int zeros = 0, sum = 0;
    for (int i = 1; i <= scc_cnt; i ++ )
        if (!dout[i]) // 如果当前连通分量出度为0
        {
            zeros ++ ; // 出度为0的连通分量个数加一
            sum += Size[i]; // 更新出度为0的连通分量中点的个数
            if (zeros > 1) // 如果出度为0的连通分量个数超过一个 说明没有一头牛被所有牛喜欢
            {
                sum = 0;
                break;
            }
        }

    cout << sum << '\n';
}

学校网络

原题链接

一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。

当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。

因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。

现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?

最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?

输入格式

第 1 行包含整数 N,表示学校数量。

第 2...N+1 行,每行包含一个或多个整数,第 i+1 行表示学校 i 应该支援的学校名单,每行最后都有一个 0 表示名单结束(只有一个 0 即表示该学校没有需要支援的学校)。

输出格式

输出两个问题的结果,每个结果占一行。

数据范围

2 ≤ N ≤ 100 2≤N≤100 2≤N≤100

输入样例

cpp 复制代码
5
2 4 3 0
4 5 0
0
0
1 0

输出样例

cpp 复制代码
1
2

题意

给出一张图:

  • 问题一:至少从多少个点出发能够遍历完图上所有点
  • 问题二:至少加多少条边能让图的强连通分量就是自身

思路

设入度为0的连通分量个数为a,出度为0的连通分量个数为b

问题一就是问a的大小

问题二就是问ab中较大的值(需要特判一下如果只有一个强连通分量,就不需要加边,输出0即可 )

举个栗子 不具体证明了:

代码

cpp 复制代码
#include <bits/stdc++.h>

using namespace std;

const int  N = 110, M = 10010;

int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
stack<int> stk;
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; // 先将dfn和low都初始化为时间戳
    stk.push(u), in_stk[u] = true; // u加入栈中

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i]; // 取出u的所有邻点j
        if (!dfn[j]) // 如果j还没被遍历
        {
            tarjan(j);
            low[u] = min(low[u], low[j]); // 用low[j]更新low[u]
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]); // 如果j已入栈 则用dfn[j]更新low[u]
    }

    if (dfn[u] == low[u]) // 如果该点是所在强连通分量的最高点
    {
        ++ scc_cnt; // 强连通分量数量加一
        int y;
        do {
            y = stk.top(); // 取出栈顶元素
            stk.pop();
            in_stk[y] = false;

            id[y] = scc_cnt; // 标记每个点所在的连通分量编号
        } while (y != u); // 直到取到此连通分量的最高点为止
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    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; j = ne[j])
        {
            int k = e[j]; // 遍历i的所有邻点k
            int a = id[i], b = id[k]; // 记录ik所在连通分量编号
            if (a != b) // 如果ik不在同一个连通分量 就在两个连通分量之间连一条i指向k的边
            {
                dout[a] ++ ;
                din[b] ++ ;
            }
        }

    int a = 0, b = 0;
    for (int i = 1; i <= scc_cnt; i ++ )
    {
        if (!din[i]) a ++ ; // 记录入度为0的点个数
        if (!dout[i]) b ++ ; // 记录出度为0的点个数
    }

    cout << a << '\n';
    if (scc_cnt == 1) cout << 0 << '\n';
    else cout << max(a, b) << '\n';
}
相关推荐
小冉在学习16 小时前
day60 图论章节刷题Part10(Floyd 算法、A * 算法)
算法·图论
XXXJessie17 小时前
acwing算法基础03-递归,枚举
算法·深度优先·图论
chan_lay2 天前
图论导引 - 第三章 第三节:哈密顿图 - 11/11
图论
汉克老师3 天前
GESP4级考试语法知识(贪心算法(二))
开发语言·数据结构·c++·算法·贪心算法·图论·1024程序员节
yangmc043 天前
二维前缀和 子矩阵的和
c语言·数据结构·c++·git·算法·矩阵·图论
5pace3 天前
GNN系统学习:简单图论、环境配置、PyG中图与图数据集的表示和使用
学习·图论
chan_lay3 天前
图论导引 - 第三章 第二节:欧拉图 - 11/10
图论
patrickpdx4 天前
【图论】图的C++实现代码
c++·图论
chan_lay4 天前
图论导引 - 第二章 - 11/08
图论
小冉在学习4 天前
day55 图论章节刷题Part07([53.寻宝]prim算法、kruskal算法)
java·算法·图论