AcWing 1174. 受欢迎的牛

AcWing 1174. 受欢迎的牛

一、题目描述

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

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

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

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

输入格式
第一行两个数 N,M

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

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

数据范围
1N104,1M5×104

输入样例

3 3
1 2
2 1
2 3

输出样例

1

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

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

1. 基础概念

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

  • 强连通:在一个有向图G中,两个点aba可以走到bb可以走到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森林的话,uv甚至可以在不同的树上
在很多算法中,后向边都是有作用的,但是前向边和横叉边的作用往往被淡化,其实它们没有太大作用,上面的③就是横叉边。

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

三、本题思路

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

暴力解法

如果暴力做的话,对于每个点都要dfs或者bfs,看是否所有点都可以到达该点,做一遍的时间复杂度是O(n+m),那么n个点,时间复杂度是O(n×(n+m)),这里的n1wm5w,时间限制是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 @   糖豆爸爸  阅读(231)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2020-04-01 双网情况下的路由配置
2014-04-01 数据库异步写入功能概要设计
Live2D
点击右上角即可分享
微信分享提示