题解:AcWing 1174 受欢迎的牛

【题目来源】

AcWiing:1174. 受欢迎的牛 - AcWing题库

【题目描述】

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

现在有 \(N\) 头牛,编号从 \(1\) 到 \(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

【核心思想】

  1. 问题分析:给定 \(N\) 头牛和 \(M\) 对关系 \((A, B)\) 表示 \(A\) 认为 \(B\) 受欢迎,关系具有传递性。求被除自己之外的所有牛认为是受欢迎的牛的数量。这是一个强连通分量(SCC)+ 缩点问题,关键在于将互相可达的牛缩成一个分量,然后分析分量之间的可达关系。

  2. 算法选择

    • Tarjan算法:使用DFS求强连通分量,时间复杂度 \(O(N + M)\)
    • 缩点:将每个强连通分量视为一个超级节点,构建缩点后的DAG(有向无环图)
    • 出度分析:统计缩点后每个分量的出度,出度为0的分量中的牛即为答案
  3. 关键步骤

    • 建图:读取 \(M\) 条有向边,构建邻接表
    • Tarjan求SCC
      • dfn[x]:节点 \(x\) 的DFS时间戳
      • low[x]:节点 \(x\) 能回溯到的最早时间戳
      • 使用栈维护当前DFS路径上的节点
      • dfn[x] == low[x] 时,找到一个强连通分量的根,弹出栈中节点直到 \(x\)
    • 缩点统计出度
      • 遍历所有边 \((u, v)\),若 scc[u] != scc[v],则分量 scc[u] 的出度加1
    • 判断答案
      • 统计出度为0的分量数量 zeros
      • zeros > 1:有多个出度为0的分量,它们互相不可达,答案为0
      • zeros == 1:唯一的出度为0的分量中的牛被所有牛认为受欢迎,答案为该分量的节点数
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + M)\),Tarjan算法线性时间
    • 空间复杂度:\(O(N + M)\),邻接表和辅助数组
  5. 强连通分量与缩点的核心思想

    • 强连通分量:有向图中互相可达的最大子图,分量内所有节点可以视为等价
    • 缩点思想:将每个SCC收缩为一个超级节点,原图变为DAG,简化问题
    • 出度为0的性质:在缩点后的DAG中,出度为0的分量无法到达其他分量,若只有一个这样的分量,则该分量可以被所有其他分量到达
    • 传递闭包简化:通过SCC缩点,将复杂的传递关系简化为DAG上的可达性问题
    • 适用于有向图中互相可达性分析、 popularity 计算、传递闭包类问题

【算法标签】

强连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

// ================= 常量与全局变量 =================

const int N = 10005;   // 最大点数

int n, m;              // n: 点数, m: 边数
vector<int> e[N];      // 邻接表存图

// ---------- Tarjan 相关 ----------
int dfn[N];            // 时间戳:节点第一次被访问的顺序
int low[N];            // 能追溯到的最早栈中节点的时间戳
int tot;               // 时间戳计数器

int stk[N];            // 手写栈,存储当前 DFS 路径上的节点
int instk[N];          // 标记节点是否在栈中
int top;               // 栈顶指针

// ---------- 强连通分量(SCC)相关 ----------
int scc[N];            // 每个节点所属的强连通分量编号
int cnt;               // 强连通分量的总数
int siz[N];            // 每个强连通分量包含的节点数

int din[N];            // 每个 SCC 的入度(本题未使用)
int dout[N];           // 每个 SCC 的出度

// ================= Tarjan 算法 =================
void tarjan(int x)
{
    dfn[x] = low[x] = ++tot;   // 初始化时间戳
    stk[++top] = x;             // 当前节点入栈
    instk[x] = 1;               // 标记在栈中

    // 遍历 x 的所有出边
    for (int y : e[x])
    {
        if (!dfn[y])            // y 尚未访问
        {
            tarjan(y);          // 递归处理 y
            low[x] = min(low[x], low[y]);   // 用子树更新 low
        }
        else if (instk[y])      // y 仍在栈中(返祖边)
        {
            low[x] = min(low[x], dfn[y]);   // 用回边更新 low
        }
    }

    // 若 x 是强连通分量的根
    if (dfn[x] == low[x])
    {
        int y;
        ++cnt;                  // 新开一个强连通分量
        do
        {
            y = stk[top--];     // 弹出栈顶
            instk[y] = 0;       // 取消栈中标记
            scc[y] = cnt;       // 归属到当前 SCC
            ++siz[cnt];         // 当前 SCC 节点数 +1
        }
        while (y != x);         // 直到弹出了 x
    }
}

// ================= 主函数 =================
int main()
{
    cin >> n >> m;

    // 读入有向边
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
    }

    // 对所有未访问的点跑 Tarjan
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);

    // ========= 缩点后统计出度 =========
    for (int x = 1; x <= n; x++)
        for (int y : e[x])
            if (scc[x] != scc[y])   // 不在同一个 SCC 中
                ++dout[scc[x]];      // x 所在 SCC 出度 +1

    // ========= 找出度为 0 的 SCC =========
    int sum = 0;        // 出度为 0 的 SCC 的节点数
    int zeros = 0;      // 出度为 0 的 SCC 数量

    for (int i = 1; i <= cnt; i++)
        if (dout[i] == 0)
        {
            sum = siz[i];
            ++zeros;
        }

    // 若有多个出度为 0 的 SCC,则无法唯一到达
    if (zeros > 1)
        sum = 0;

    cout << sum << endl;
    return 0;
}

【运行结果】

3 3
1 2
2 1
2 3
1
posted @ 2026-06-27 14:19  团爸讲算法  阅读(6)  评论(0)    收藏  举报