Files
python/TangDou/AcWing/DCC/NC15707.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

151 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [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$。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230726083936.png)
如图,要想找到一个点集,使得从这些点出发能够到达任意一点,显然这个点集是$5$和$6$。$5$和$6$也都是一个独立的连通分量。
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230726084053.png)
如图,若是$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$可以避免用数组后再排序