AcWing 1174. 受欢迎的牛

\(AcWing\) \(1174\). 受欢迎的牛

一、题目描述

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

现在有 \(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

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

二、\(Tarjan\)算法求有向图强连通分量

1. 基础概念

首先了解几个概念:强连通强连通图强连通分量

  • 强连通:在一个有向图\(G\)中,两个点\(a,b\)\(a\)可以走到\(b\)\(b\)可以走到\(a\),我们就说\((a,b)\)强连通

  • 强连通图:在一个有向图\(G\)中,任意两个点都是强连通

  • 强连通分量:在一个有向图\(G\)中,有一个子图,它任意两个点都是强连通,我们就说这个子图为强连通分量,特别的一个点也是一个强连通分量

如图:

显然可得:\(1,2,3,5\) 构成了一个强连通分量(一个点也是

代码实现时的设计

  • \(dfn[v]\):搜索节点\(v\)时的时间序
  • \(low[x]\):以\(x\)为根的子树中,每个节点中连接的点的时间戳的 最小值
    初值化:\(low[x]=dfn[x]\)

2. 有向图的强连通分量用途

主要是通过 缩点(将强连通分量缩成一个点),把有向图,转换为 有向无环图(拓扑图,\(DAG\))。

如下图,左图圈内的是一个强连通分量,通过缩点,转化为右图。这种做法其实有很多应用,比如求最短路等。

3. 算法步骤

  • ① 初始化:

    • 给每个顶点分配一个唯一的标识号
    • 初始化一个空栈
    • 初始化一个访问标记数组,用于记录每个顶点是否已经被访问
  • ② 对于图中的每个未被访问的顶点执行步骤\(3\)

  • ③ 遍历顶点:

    • 给当前顶点设置一个访问标记并将其压入栈中
    • 将当前顶点的访问次序(标记号)和最小后序号(\(low\)值)都设置为当前最小值
    • 遍历当前顶点的所有邻居节点:
      • 如果邻居节点未被访问,则对邻居节点执行步骤\(3\)
        • 在递归步骤中,如果邻居节点没有被访问过,将它的父节点设置为当前节点
      • 如果邻居节点已经在栈中,更新当前顶点的最小后序号(\(low\)值)
    • 如果当前顶点是一个 根节点,则弹出栈中从当前顶点开始的所有顶点,并将它们组成一个强连通分量。
  • ④ 返回所有的强连通分量

4. 算法实现

// Tarjan算法求强连通分量
int stk[N], top, in_stk[N]; // stk[N]:堆栈,top:配合堆栈使用的游标top,in_stk[N]:是否在栈内
int dfn[N], ts, low[N];     // dfn[N]:时间戳记录数组,ts:时间戳游标, low[N]:从u开始走所能遍历到的最小时间戳
int id[N], scc_cnt, sz[N];  // id[N]:强连通分量块的编号,scc_cnt:强连通分量的编号游标,sz[i]:编号为i的强连通分量中原来点的个数

void tarjan(int u) {
    dfn[u] = low[u] = ++ts; // 初始时间戳
    stk[++top] = u;         // 入栈
    in_stk[u] = 1;          // 在栈内

    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {                    // v未访问过
            tarjan(v);                    // dfs深搜
            low[u] = min(low[u], low[v]); // 更新low[u]
        } else if (in_stk[v])             // v已经栈中,找到强连通分量
            low[u] = min(low[u], low[v]); // 更新low[u]
    }
    // 发现强连通分量
    if (dfn[u] == low[u]) {
        ++scc_cnt;
        int x;
        do {
            x = stk[top--];
            in_stk[x] = 0;
            id[x] = scc_cnt; // 记录x是缩点后编号为ssc_cnt号强连通分量的一员
            sz[scc_cnt]++;   // 缩点后编号为ssc_cnt号强连通分量中的成员个数+1
        } while (x != u);
    }
}
答疑解惑
for (int i = h[u]; ~i; i = ne[i]) {
    int v = e[i];
    if (!dfn[v]) {                    // v未访问过
        tarjan(v);                    // dfs深搜
        low[u] = min(low[u], low[v]); // 更新low[u]
    } else if (in_stk[v])                 // v已经栈中,找到强连通分量
        low[u] = min(low[u], low[v]);
}

这段代码中的节点\(u\)和节点\(v\)可能存在三种情况,分别如下:

  • ① 节点\(v\)未被访问过(\(!dfn[v]\)):这种情况说明节点\(v\)是一个未探索的节点,还没有被访问过。在深度优先搜索中,我们需要继续从节点\(u\)探索到节点\(v\)

  • ② 节点\(v\)已经在栈内(\(in\_stk[v]\)):这种情况表示节点\(v\)已经访问过,并且它当前在栈中。这表示节点\(v\)与节点\(u\)在同一个强连通分量中,即形成了一个环。

  • ③ 节点\(v\)已被访问过但不在栈内:这种情况发生在节点\(v\)已经访问过,并且已经出栈。在这种情况下,我们不需要做任何操作,因为节点\(v\)已经被处理过并且不再对强连通分量的构建产生影响。

注意:树边,后向边,前向边,都有祖先,后裔的关系,但横叉边没有,\(u->v\)为横叉边,说明在这棵\(DFS\)树中,它们不是祖先后裔的关系它们可能是兄弟关系,堂兄弟关系,甚至更远的关系,如果是\(dfs\)森林的话,\(u\)\(v\)甚至可以在不同的树上
在很多算法中,后向边都是有作用的,但是前向边和横叉边的作用往往被淡化,其实它们没有太大作用,上面的③就是横叉边。

以上三种情况涵盖了节点\(u\)与节点\(v\)之间的所有可能情况。根据具体的情况,算法会执行相应的操作,例如继续深度优先搜索、更新\(low[u]\)值或者识别出一个强连通分量并进行缩点操作。

三、本题思路

题意是找到被其他所有牛都欢迎的牛的数量。在有向图的角度,就是 所有的点都可以走到当前这个点

暴力解法

如果暴力做的话,对于每个点都要\(dfs\)或者\(bfs\),看是否所有点都可以到达该点,做一遍的时间复杂度是\(O(n + m)\),那么\(n\)个点,时间复杂度是\(O(n \times (n+m))\),这里的\(n\)\(1w\)\(m\)\(5w\),时间限制是\(1s\),会超时。

优化解法

如果是拓扑图(有向无环图\(DAG\))的话,就好解决了。

结论只需要统计出度为\(0\)的点的数量。如果出度为\(0\)的点的数量大于等于\(2\),那么一定不存在最受欢迎的牛

解释:有向无环图中,如果有\(2\)个或\(2\)个以上的点出度为\(0\),那么其中\(1\)个叶子结点必然有到不了的点,也就是不存在最受欢迎的牛。画图理解:

如果只存在一个出度为\(0\)的点呢?

综上,如果是拓扑图的话,这道题就好做了。实际上,我们可以 用强连通分量算法将图转换为拓扑图

方法
① 先求出该图的强连通分量,然后缩点,变成 有向无环图\(DAG\),再统计一下每个点的出度即可。
② 找到出度为\(0\)的点的数量为\(1\)的情况,然后统计该点所表示的强连通分量,其中包含多少个点,这里的所有点都可以被其他所有点走到。

\(Code\)

#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = 50010;
int n, m; // n个点,m条边
int d[N]; // 记录一下每个强连通分量的出度

// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

// Tarjan算法求强连通分量
int stk[N], top, in_stk[N]; // stk[N]:堆栈,top:配合堆栈使用的游标top,in_stk[N]:是否在栈内
int dfn[N], ts, low[N];     // dfn[N]:时间戳记录数组,ts:时间戳游标, low[N]:从u开始走所能遍历到的最小时间戳
int id[N], scc_cnt, sz[N];  // id[N]:强连通分量块的编号,scc_cnt:强连通分量的编号游标,sz[i]:编号为i的强连通分量中原来点的个数

void tarjan(int u) {
    dfn[u] = low[u] = ++ts; // 初始时间戳
    stk[++top] = u;         // 入栈
    in_stk[u] = 1;          // 在栈内

    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {                    // v未访问过
            tarjan(v);                    // dfs深搜
            low[u] = min(low[u], low[v]); // 更新low[u]
        } else if (in_stk[v])             // v已经栈中,找到强连通分量
            low[u] = min(low[u], dfn[v]); // 更新low[u]
    }
    // 发现强连通分量
    if (dfn[u] == low[u]) {
        ++scc_cnt;
        int x;
        do {
            x = stk[top--];
            in_stk[x] = 0;
            id[x] = scc_cnt; // 记录x是缩点后编号为ssc_cnt号强连通分量的一员
            sz[scc_cnt]++;   // 缩点后编号为ssc_cnt号强连通分量中的成员个数+1
        } while (x != u);
    }
}

int main() {
    scanf("%d %d", &n, &m);
    memset(h, -1, sizeof h); // 初始化邻接表
    for (int i = 1; i <= m; i++) {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b);
    }
    //(1)求强连通分量,缩点
    for (int i = 1; i <= n; i++) // 需示枚举每个点出发尝试,否则会出现有的点没有遍历到的情况
        if (!dfn[i]) tarjan(i);

    //(2)缩点后其实就是一个DAG,计算出DAG中每个新节点的出度
    for (int u = 1; u <= n; u++) // 枚举原图中每个出边,用法好怪异~
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];             // u->v
            int a = id[u], b = id[v]; // u,v分别在哪个强连通分量中
            if (a != b) d[a]++;       // a号强连通分量,也可以理解为是缩点后的点号,出度++
        }

    //(3) 出度为0的只能有1个,如果大于1个,就无解,如果正好是一个,返回此强连通分量中结点的个数
    int zeros = 0, res = 0;
    for (int i = 1; i <= scc_cnt; i++) // 枚举强连通分量块
        if (!d[i]) {                   // 如果出度是0
            zeros++;                   // 出度是0的强连通分量个数+1
            res = sz[i];               // 累加此强连通分量中点的个数
            if (zeros > 1) {           // 如果强连通分量块的数量大于1个,则无解
                res = 0;
                break;
            }
        }

    //(4)求有多少头牛被除自己之外的所有牛认为是受欢迎的
    printf("%d\n", res);
    return 0;
}
posted @ 2022-04-01 10:11  糖豆爸爸  阅读(209)  评论(0编辑  收藏  举报
Live2D