|
|
|
|
## [NC15707 可达性](https://ac.nowcoder.com/acm/problem/15707?&headNav=acm)
|
|
|
|
|
|
|
|
|
|
时间限制:$C/C++$ $1$秒,其他语言$2$秒
|
|
|
|
|
空间限制:$C/C++$ $262144K$,其他语言$524288K$
|
|
|
|
|
$64bit$ $IO$ $Format: \%lld$
|
|
|
|
|
|
|
|
|
|
#### 题目描述
|
|
|
|
|
给出一个 $0 ≤ N ≤ 10^5$ 点数、$0 ≤ M ≤ 10^5$ 边数的 **有向图**,
|
|
|
|
|
输出一个尽可能小的点集,使得从这些点出发能够到达任意一点,如果有多个这样的集合,输出这些集合升序排序后字典序最小的。
|
|
|
|
|
|
|
|
|
|
**输入描述**:
|
|
|
|
|
第一行为两个整数 $1 ≤ n, m ≤ 10^5$,
|
|
|
|
|
接下来 $M$ 行,每行两个整数 $1 ≤ u, v ≤ 10^5$ 表示从点 $u$ 至点 $v$ 有一条有向边。
|
|
|
|
|
数据保证没有重边、自环。
|
|
|
|
|
|
|
|
|
|
**输出描述**:
|
|
|
|
|
第一行输出一个整数 $z$,表示作为答案的点集的大小;
|
|
|
|
|
第二行输出 $z$ 个整数,升序排序,表示作为答案的点集。
|
|
|
|
|
|
|
|
|
|
示例$1$
|
|
|
|
|
|
|
|
|
|
**输入**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
7 10
|
|
|
|
|
4 5
|
|
|
|
|
5 1
|
|
|
|
|
2 5
|
|
|
|
|
6 5
|
|
|
|
|
7 2
|
|
|
|
|
4 2
|
|
|
|
|
1 2
|
|
|
|
|
5 3
|
|
|
|
|
3 5
|
|
|
|
|
3 6
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**输出**
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
2
|
|
|
|
|
4 7
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 思路
|
|
|
|
|
让求一个点集,使得从这些点出发能够到达任意一点,即是求能否找到一些强连通分量,这些强连通分量的入度为$0$。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
如图,要想找到一个点集,使得从这些点出发能够到达任意一点,显然这个点集是$5$和$6$。$5$和$6$也都是一个独立的连通分量。
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
如图,若是$5$和$6$同属于一个强连通分量,根据题目要求,让尽可能小的点集和字典序最小的,那输出一个$5$就可以了。
|
|
|
|
|
|
|
|
|
|
因此,利用$tarjan$缩点,找出所有入度为$0$的强连通分量,找出里面最小一个的点。最后将所有的点再从小到大排序输出就行了。
|
|
|
|
|
|
|
|
|
|
### $Code$
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 1e5 + 10, M = N << 1;
|
|
|
|
|
|
|
|
|
|
int n, m;
|
|
|
|
|
int in[N]; // 入度数组
|
|
|
|
|
bool flag[N]; // 用于判断所在连通分量是否是目标连通分量,是则为1
|
|
|
|
|
|
|
|
|
|
// 链式前向星
|
|
|
|
|
int e[M], h[N], idx, w[M], ne[M];
|
|
|
|
|
void add(int a, int b, int c = 0) {
|
|
|
|
|
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 有向图求强连通分量
|
|
|
|
|
int stk[N], top; // tarjan算法需要用到的堆栈
|
|
|
|
|
bool in_stk[N]; // 是否在栈内
|
|
|
|
|
int dfn[N]; // dfs遍历到u的时间
|
|
|
|
|
int low[N]; // 从u开始走所能遍历到的最小时间戳
|
|
|
|
|
int ts; // 时间戳,dfs序的标识,记录谁先谁后
|
|
|
|
|
int id[N], scc_cnt; // 强连通分量块的最新索引号
|
|
|
|
|
int sz[N]; // sz[i]表示编号为i的强连通分量中原来点的个数
|
|
|
|
|
// tarjan算法求强连通分量
|
|
|
|
|
void tarjan(int u) {
|
|
|
|
|
dfn[u] = low[u] = ++ts;
|
|
|
|
|
stk[++top] = u;
|
|
|
|
|
in_stk[u] = 1;
|
|
|
|
|
for (int i = h[u]; ~i; i = ne[i]) {
|
|
|
|
|
int v = e[i];
|
|
|
|
|
if (!dfn[v]) {
|
|
|
|
|
tarjan(v);
|
|
|
|
|
low[u] = min(low[u], low[v]);
|
|
|
|
|
} else if (in_stk[v])
|
|
|
|
|
low[u] = min(low[u], dfn[v]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dfn[u] == low[u]) {
|
|
|
|
|
++scc_cnt; // 强连通分量的序号
|
|
|
|
|
int x; // 临时变量x,用于枚举栈中当前强连通分量中每个节点
|
|
|
|
|
do {
|
|
|
|
|
x = stk[top--]; // 弹出节点
|
|
|
|
|
in_stk[x] = false; // 标识不在栈中了
|
|
|
|
|
id[x] = scc_cnt; // 记录每个节点在哪个强连通分量中
|
|
|
|
|
sz[scc_cnt]++; // 这个强连通分量中节点的个数+1
|
|
|
|
|
} while (x != 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); // 有向图
|
|
|
|
|
}
|
|
|
|
|
// Tarjan算法套路
|
|
|
|
|
for (int i = 1; i <= n; i++) // 疑问:如果有多个彼此不连通的连通块,那是不是不存在可以到达所有点的点集?
|
|
|
|
|
if (!dfn[i]) tarjan(i);
|
|
|
|
|
|
|
|
|
|
// 枚举所有出边
|
|
|
|
|
for (int u = 1; u <= n; u++)
|
|
|
|
|
for (int i = h[u]; ~i; i = ne[i]) {
|
|
|
|
|
int v = e[i];
|
|
|
|
|
int a = id[u], b = id[v]; // u和v不属于同一个强连通分量,且是u ->v
|
|
|
|
|
if (a != b) in[b]++; // 则v所在的强连通分量的入度+1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 遍历每个强连通分量,找出入度为0的强连通分量
|
|
|
|
|
for (int i = 1; i <= scc_cnt; i++)
|
|
|
|
|
if (!in[i]) flag[i] = 1;
|
|
|
|
|
|
|
|
|
|
// 遍历每个节点,如果它所在有强连通分量是入度为零的,则把此点记录到答案数组中,
|
|
|
|
|
// 并且,修改此强连通分量的入度不是0,防止再次被加入到答案数组中
|
|
|
|
|
set<int> ans;
|
|
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
|
|
int x = id[i];
|
|
|
|
|
if (flag[x]) {
|
|
|
|
|
ans.insert(i);
|
|
|
|
|
flag[x] = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cout << ans.size() << endl;
|
|
|
|
|
for (auto x : ans) cout << x << " ";
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 总结
|
|
|
|
|
- ① $Tarjan$求出强连通分量后,再利用$1 \sim n$枚举所有边,统计一下每条边的两个端点是否在同一个$scc$中,统计每个$scc$的入度
|
|
|
|
|
- ② 利用$1 \sim scc_cnt$的循环,对于入度为零的$scc$进行标识
|
|
|
|
|
- ③ 从$1 \sim n$由小到大枚举每个点,发现所在$scc$的 入度为零,则将该点记录到答案中,并且设置$flag[id]=0$,这个非常妙,可以有效防止后续该$scc$中其它大的序号点入答案
|
|
|
|
|
- ④ 用$set$可以避免用数组后再排序
|