题解:洛谷 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
【核心思想】
-
问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向图,求点数大于 \(1\) 的强连通分量(SCC)个数。强连通分量是指分量内任意两点互相可达的最大子图。本质上是Tarjan 算法问题:通过 DFS 和栈维护,利用时间戳和回溯值识别 SCC。
-
算法选择:
- Tarjan 算法:单次 DFS 即可求出所有 SCC,时间复杂度 \(O(n + m)\)
- 栈维护:用栈记录当前 DFS 路径上的节点,当
dfn[x] == low[x]时弹出栈顶元素形成一个 SCC
-
关键步骤:
- 读取数据:读入 \(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])\)
- 若 \(y\) 未访问:
- 若 \(dfn[x] == low[x]\):弹出栈顶直到 \(x\),形成一个 SCC,记录大小
siz[cnt]
- \(dfn[x] = low[x] = ++tot\),\(x\) 入栈,标记
- 统计答案:遍历所有 SCC,统计
siz[i] > 1的个数 - 输出答案
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m)\),每个节点和边访问一次
- 空间复杂度:\(O(n + m)\),邻接表和栈
-
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
浙公网安备 33010602011771号