每一头牛的愿望就是变成一头最受欢迎的牛。
现在有 N 头牛,编号从 1 到 N,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。
这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。
你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。
输入格式
第一行两个数 N,M;
接下来 M 行,每行两个数 A,B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出现多个 A,B)。
输出格式
输出被除自己之外的所有牛认为是受欢迎的牛的数量。
数据范围
1≤N≤104,
1≤M≤5×104
输入样例:
3 3
1 2
2 1
2 3
输出样例:
1
样例解释
只有第三头牛被除自己之外的所有牛认为是受欢迎的。
思路:
/*
有向图
连通分量:对于分量中任意两点u,v 必然可以从u走到v 且从v走到u
强连通分量:极大连通分量
有向图 → 有向无环图(DAG)
缩点(将所有连通分量缩成一个点)
缩点举例:
o→o→o→o
↑ ↓
o→o→o→o
中间的环缩成一个点
o o
↘ ↗
o
↗ ↘
o o
应用:
求最短/最长路 递推
求强连通分量:dfs
1 树枝边(x,y)
o
/ \
o o
/ / \
o o o
2 前向边(x,y)
o
/ \
o x
/ ↙ \
o y o
3 后向边
o
/ \
o y
/ ↗ \
o x o
4横插边(往已经搜过的路径上的点继续深搜)
因为我们是从左往右搜的 所以一般是x左边分支上的点
o
/ \
o o
/ / \
y ← x o
如果往x右边边分支上的点搜 则属于树枝边
强连通分量:
情况1:
x存在后向边指向祖先结点y 直接构成环
o
/ \
o y
/ ↗/\
o x o
情况2:
x存在横插边指向的点y有指向x和y的公共祖先节点及以上的点的边
再通过根节点往下走到x间接构成环
o
↗ / \
↗ o o
↗ / / \
y ← x o
Tarjan 算法求强连通分量
引入 时间戳(按dfs 回溯的顺序标记)
1
↗ / \
↗ 2 4
↗ / / \
3 ← 5 6
标记上时间后:
dfn[u]dfs遍历到u的时间(如上图中的数字)
low[u]从u开始走所能遍历到的最小时间戳(上图中1,2,3,4,5都是一个环/强连通分量中的
即dfn[1]=low[1]=low[2]=low[3]=low[4]=low[5])
--即u如果在强连通分量,其所指向的层数最高的点
u是其所在的强连通分量的最高点 (上图中dfn[1]=low[1] dfn[6]=low[6])
<=>
dfn[u] == low[u]
树枝边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
前向边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
后向边(x,y) 中dfn[x]>dfn[y] 后向边的终点dfn[u] == low[u]
横插边(x,y) 中dfn[x]>dfn[y]
缩点
for i=1;i<=n;i++
for i的所有邻点j
if i和j不在同一scc中:
加一条新边id[i]→id[j]
缩点操作后变成有向无环图
就能做topo排序了(此时连通分量编号id[]递减的顺序就是topo序了)
因为我们++scc_cnt是在dfs完节点i的子节点j后才判断low[u]==dfn[u]后才加的
那么子节点j如果是强连通分量 scc_idx[j]一定小于scc_idx[i]
线性复杂度 一遍dfs就行
*/
代码:
/*
本题
当一个强连通的出度为0,则该强连通分量中的所有点都被其他强连通分量的牛欢迎
但假如存在两及以上个出度=0的牛(强连通分量) 则必然有一头牛(强连通分量)不被所有牛欢迎
见下图最右边两个强连通分量
o→o→o
↑
o→o
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, M = 50010;
int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
/*
在一个有向的拓扑图,两点之间是不一定联通的,也就是说从一个点出发走不到所有的点。所以做一次tarjan算法从起点出发缩点的范围是起点能够走到的所有点,这样操作之后每一个从起点出发的缩点就是非并行的。
比如后遍历的起点dfn是50,但是搜到一个之前已经遍历过的dfn为1的点,这时候显然是一个前向边,满足拓扑序的,但是low[u]就会全部更新成为u,就很僵硬。
*/
int id[N], scc_cnt, Size[N];
int dout[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void tarjan(int u) // 缩点的过程
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u, in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!dfn[j]) // 没访问过就继续
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) // 访问过看看标记是不是比当前值小
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u]) //该强连通分量第一个访问的点
{
int y;
++scc_cnt;
do
{
y = stk[top--];
Size[scc_cnt]++;
id[y] = scc_cnt;
in_stk[y] = false;
} while (y != u);
}
}
int main()
{
memset(h, -1, sizeof(h));
scanf("%d %d", &n, &m);
while (m--)
{
int a, b;
scanf("%d %d", &a, &b);
add(a, b);
}
for (int i = 1; i <= n; i++) // 深度优先
{
if (!dfn[i])
{
tarjan(i);
}
}
for (int u = 1; u <= n; u++) // 统计出度为0的强连通分量 后面累加
{
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (id[u] != id[j])
{
dout[id[u]]++;
}
}
}
int zeros = 0, res = 0;
for (int i = 1; i <= scc_cnt; i++)
{
if (!dout[i]) // 只需要判断是否等于0 所以强连通分量之间有多条边也无所谓
{
zeros++;
res += Size[i];
if (zeros > 1)
{
res = 0;
break;
}
}
}
printf("%d\n", res);
return 0;
}