题解:洛谷 P2863 The Cow Prom

【题目来源】

洛谷:P2863 [USACO06JAN] The Cow Prom S - 洛谷

【题目描述】

有一个 \(n\) 个点,\(m\) 条边的有向图,请求出这个图点数大于 \(1\) 的强连通分量个数。

【输入】

第一行为两个整数 \(n\)\(m\)

第二行至 \(m+1\) 行,每一行有两个整数 \(a\)\(b\),表示有一条从 \(a\)\(b\) 的有向边。

【输出】

仅一行,表示点数大于 \(1\) 的强连通分量个数。

【输入样例】

5 4
2 4
3 5
1 2
4 1

【输出样例】

1

【核心思想】

  1. 问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向图,求点数大于 \(1\) 的强连通分量(SCC)个数。强连通分量是指分量内任意两点互相可达的最大子图。本质上是Tarjan 算法问题:通过 DFS 和栈维护,利用时间戳和回溯值识别 SCC。

  2. 算法选择

    • Tarjan 算法:单次 DFS 即可求出所有 SCC,时间复杂度 \(O(n + m)\)
    • 栈维护:用栈记录当前 DFS 路径上的节点,当 dfn[x] == low[x] 时弹出栈顶元素形成一个 SCC
  3. 关键步骤

    • 读取数据:读入 \(n, m\)\(m\) 条有向边,建邻接表
    • Tarjan DFS(对每个未访问节点 \(i\)):
      • \(dfn[x] = low[x] = ++tot\)\(x\) 入栈,标记 instk[x] = 1
      • 遍历邻接点 \(y\)
        • \(y\) 未访问:tarjan(y)\(low[x] = \min(low[x], low[y])\)
        • \(y\) 在栈中:\(low[x] = \min(low[x], dfn[y])\)
      • \(dfn[x] == low[x]\):弹出栈顶直到 \(x\),形成一个 SCC,记录大小 siz[cnt]
    • 统计答案:遍历所有 SCC,统计 siz[i] > 1 的个数
    • 输出答案
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m)\),每个节点和边访问一次
    • 空间复杂度:\(O(n + m)\),邻接表和栈
  5. Tarjan 算法的核心思想

    • 时间戳与回溯值dfn[x] 记录 \(x\) 的访问顺序,low[x] 记录 \(x\) 及其后代能回溯到的最早祖先的 dfn
    • SCC 的判定dfn[x] == low[x] 意味着 \(x\) 是某个 SCC 的"根",栈中从 \(x\) 到栈顶的所有节点构成一个 SCC
    • 栈的作用:栈中保存的是当前 DFS 路径上尚未确定 SCC 的节点,已确定 SCC 的节点被弹出
    • 横叉边的处理:若 \(y\) 已访问但不在栈中,说明 \(y\) 属于已完成的 SCC,不影响 \(low[x]\)
    • 适用于有向图分解、强连通分量、缩点类问题

【算法标签】

普及+ #强连通分量

【代码详解】

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

const int N = 1e4 + 5;  // 定义最大节点数

// 全局变量声明
int n, m;               // n: 节点数,m: 边数
vector<int> e[N];       // 图的邻接表
int dfn[N], low[N];     // dfn: 访问时间戳,low: 最早可回溯到的节点时间戳
int tot;                // 时间戳计数器
int stk[N], instk[N];   // stk: 栈,instk: 标记节点是否在栈中
int top;                // 栈顶指针
int scc[N], siz[N];     // scc: 节点所属SCC编号,siz: 每个SCC的大小
int cnt;                // SCC计数器

/**
 * Tarjan算法求强连通分量
 * @param x 当前访问的节点
 */
void tarjan(int x)
{
    // 入栈处理:设置时间戳并入栈
    dfn[x] = low[x] = ++tot;
    stk[++top] = x;
    instk[x] = 1;

    // 遍历所有邻接节点
    for (int y : e[x])
    {
        if (!dfn[y])            // 如果y未被访问过
        {
            tarjan(y);
            low[x] = min(low[x], low[y]);  // 回溯时更新low值
        }
        else if (instk[y])      // 如果y在栈中
        {
            low[x] = min(low[x], dfn[y]);  // 更新low值
        }
    }

    // 判断是否为SCC根节点
    if (dfn[x] == low[x])
    {
        int y;
        ++cnt;  // 增加SCC计数
        do
        {
            y = stk[top--];     // 弹出栈顶元素
            instk[y] = 0;       // 标记为不在栈中
            scc[y] = cnt;       // 记录SCC编号
            ++siz[cnt];         // 增加当前SCC的大小
        } while (y != x);       // 直到处理完当前SCC的所有节点
    }
}

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 构建图的邻接表
    for (int i = 1; i <= m; i++)
    {
        int a, b;
        cin >> a >> b;
        e[a].push_back(b);
    }

    // 对每个未访问的节点执行Tarjan算法
    for (int i = 1; i <= n; i++)
    {
        if (!dfn[i])
        {
            tarjan(i);
        }
    }

    // 统计大小大于1的SCC数量
    int ans = 0;
    for (int i = 1; i <= cnt; i++)
    {
        if (siz[i] > 1)
        {
            ans++;
        }
    }

    // 输出结果
    cout << ans << endl;
    return 0;
}

【运行结果】

5 4
2 4
3 5
1 2
4 1
1
posted @ 2026-08-17 09:46  团爸讲算法  阅读(10)  评论(0)    收藏  举报