题解: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
【核心思想】
-
问题分析:给定 \(N\) 头牛和 \(M\) 对关系 \((A, B)\) 表示 \(A\) 认为 \(B\) 受欢迎,关系具有传递性。求被除自己之外的所有牛认为是受欢迎的牛的数量。这是一个强连通分量(SCC)+ 缩点问题,关键在于将互相可达的牛缩成一个分量,然后分析分量之间的可达关系。
-
算法选择:
- Tarjan算法:使用DFS求强连通分量,时间复杂度 \(O(N + M)\)
- 缩点:将每个强连通分量视为一个超级节点,构建缩点后的DAG(有向无环图)
- 出度分析:统计缩点后每个分量的出度,出度为0的分量中的牛即为答案
-
关键步骤:
- 建图:读取 \(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
- 遍历所有边 \((u, v)\),若
- 判断答案:
- 统计出度为0的分量数量
zeros - 若
zeros > 1:有多个出度为0的分量,它们互相不可达,答案为0 - 若
zeros == 1:唯一的出度为0的分量中的牛被所有牛认为受欢迎,答案为该分量的节点数
- 统计出度为0的分量数量
-
时间/空间复杂度:
- 时间复杂度:\(O(N + M)\),Tarjan算法线性时间
- 空间复杂度:\(O(N + M)\),邻接表和辅助数组
-
强连通分量与缩点的核心思想:
- 强连通分量:有向图中互相可达的最大子图,分量内所有节点可以视为等价
- 缩点思想:将每个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
浙公网安备 33010602011771号