把「认为」关系建成有向图,SCC 缩点后被全员认可等价于唯一出度为 0 的汇点,答案即该分量大小。
OJ: roj
题目 ID: 1513
难度:普及+/提高-
标签:图论强连通分量SCC缩点python
创建: 2026-09-30 15:38
更新: 2026-10-07 11:01
形式化题目
给定
求有多少个点
样例说明
样例输入的边为
graph LR
subgraph C1["分量 {1, 2}"]
n1["1"] --> n2["2"]
n2 --> n1
end
n2 --> n3["分量 {3}"]
正解
思路
朴素做法与瓶颈。 对每个点
关键观察:互相可达是等价关系。 若
汇点判定。 在缩点 DAG 中,一个分量被全体可达
- 若汇点唯一:从任意分量沿出边前进,DAG 无环必然终止,只能终止在该汇点,即所有分量都能到达它;
- 若存在第二个汇点
:两个汇点之间没有路径,原汇点不被 可达,答案为 。
缩点后所有点在一个分量内互达,所以答案 = 唯一汇点分量的大小(汇点不唯一则为
graph LR
A["{1, 2}"] --> B["{3}"]
求 SCC 用 Kosaraju 两遍 DFS。 第一遍在原图上求后序序列;第二遍按逆后序在反图上 DFS,每次从反图能走到的点集恰为一个 SCC。
最后扫一遍边:comp[u] != comp[v] 即缩点图上有边,记下这些分量;全体分量编号减去它们就是汇点集合,大小为
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 15:35
# update_at: 2026-09-30 15:35
import sys
def postorder(n: int, g: list[list[int]]) -> list[int]:
"""原图上迭代 DFS 的后序序列:Kosaraju 第一遍,后走完的点先被第二遍处理。"""
seen = [False] * n
order: list[int] = []
for start in range(n):
if seen[start]:
continue
seen[start] = True
stack = [(start, 0)] # (点, 已走到第几条出边)
while stack:
v, i = stack.pop()
if i < len(g[v]):
stack.append((v, i + 1)) # 回溯后从下一条出边继续
w = g[v][i]
if not seen[w]:
seen[w] = True
stack.append((w, 0))
else:
order.append(v) # 所有出边走完才入序,即后序
return order
def assign_components(n: int, rg: list[list[int]], order: list[int]) -> list[int]:
"""反图上按逆后序 DFS 染色,返回每个点所属强连通分量编号(从 0 连续)。"""
comp = [-1] * n
cid = 0
for start in reversed(order):
if comp[start] != -1:
continue
comp[start] = cid
stack = [start]
while stack:
v = stack.pop()
for w in rg[v]:
if comp[w] == -1: # 同一次反图 DFS 能到的点都属本分量
comp[w] = cid
stack.append(w)
cid += 1
return comp
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
g: list[list[int]] = [[] for _ in range(n)]
rg: list[list[int]] = [[] for _ in range(n)]
for _ in range(m):
a, b = next(data) - 1, next(data) - 1 # 转 0 起下标
g[a].append(b)
rg[b].append(a)
comp = assign_components(n, rg, postorder(n, g))
# 缩点 DAG 中出度为 0 的分量才能被全员认可;答案 = 唯一汇点分量的大小
has_out = {comp[u] for u, vs in enumerate(g) for v in vs if comp[v] != comp[u]}
sinks = {cid for cid in range(max(comp) + 1)} - has_out
popular = next(iter(sinks)) if len(sinks) == 1 else -1
print(sum(1 for c in comp if c == popular) if popular != -1 else 0)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 05:37
* update_at: 2026-10-05 05:37
*/
// 受欢迎的牛:Kosaraju 两遍 DFS 求 SCC 缩点,
// 被除自己外所有牛认可 <=> 缩点 DAG 中唯一出度为 0 的汇点,答案为该分量大小。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXM = 50005;
typedef long long ll;
// 链式前向星:正图 g,反图 rg
int head[MAXN], to[MAXM], nxt[MAXM], edge_cnt;
int rhead[MAXN], rto[MAXM], rnxt[MAXM], redge_cnt;
int n, m;
int comp[MAXN]; // comp[v] = 点 v 所属强连通分量编号
int out_comp[MAXN]; // out_comp[c] = 分量 c 在缩点图上是否有指向别的分量的出边
bool vis[MAXN];
int order_[MAXN]; // 第一遍 DFS 的后序序列
int order_cnt;
int stk[MAXN]; // 迭代 DFS 的显式栈:stk 存点,edge_ptr 存已走到第几条边
int edge_ptr[MAXN];
ll ans;
// 正图加边 u -> v。
void add_edge(int u, int v) {
edge_cnt++;
to[edge_cnt] = v;
nxt[edge_cnt] = head[u];
head[u] = edge_cnt;
}
// 反图加边(方向取反)。
void add_redge(int u, int v) {
redge_cnt++;
rto[redge_cnt] = v;
rnxt[redge_cnt] = rhead[u];
rhead[u] = redge_cnt;
}
// 第一遍 DFS:原图上求后序序列,走完一个点的全部出边才入序。
// N 达 1e4,递归可能爆栈,用显式栈迭代实现。
void dfs_order(int s) {
int top = 0;
stk[top++] = s;
edge_ptr[s] = head[s];
vis[s] = true;
while (top > 0) {
int u = stk[top - 1];
int e = edge_ptr[u];
if (e != 0) {
edge_ptr[u] = nxt[e]; // 记录回溯后从下一条出边继续
int v = to[e];
if (!vis[v]) {
vis[v] = true;
stk[top++] = v;
edge_ptr[v] = head[v];
}
} else {
// 出边全部走完,记入后序
top--;
order_cnt++;
order_[order_cnt] = u;
}
}
}
// 第二遍 DFS:反图上从 s 能走到的点都属于同一个 SCC。
void dfs_assign(int s, int cid) {
int top = 0;
stk[top++] = s;
comp[s] = cid;
while (top > 0) {
int u = stk[--top];
for (int e = rhead[u]; e != 0; e = rnxt[e]) {
int v = rto[e];
if (comp[v] == 0) {
comp[v] = cid;
stk[top++] = v;
}
}
}
}
void solve() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int a, b;
cin >> a >> b;
add_edge(a, b);
add_redge(b, a);
}
// Kosaraju 第一遍:对每个未访问点求后序
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
dfs_order(i);
}
}
// 第二遍:按逆后序在反图上染色
int cid = 0; // 分量编号从 1 开始,comp[v] == 0 表示未染色
for (int i = n; i >= 1; i--) {
int u = order_[i];
if (comp[u] == 0) {
cid++;
dfs_assign(u, cid);
}
}
// 扫一遍边:comp[u] != comp[v] 即缩点图上有出边,标记该分量不是汇点
for (int u = 1; u <= n; u++) {
for (int e = head[u]; e != 0; e = nxt[e]) {
int v = to[e];
if (comp[u] != comp[v]) {
out_comp[comp[u]] = 1;
}
}
}
// 汇点唯一时答案为该分量大小;有多个汇点则答案为 0
int sink = 0;
bool unique_sink = true;
for (int c = 1; c <= cid; c++) {
if (out_comp[c] == 0) {
if (sink != 0) {
unique_sink = false; // 出现第二个汇点
}
sink = c;
}
}
if (!unique_sink) {
ans = 0;
} else {
for (int v = 1; v <= n; v++) {
if (comp[v] == sink) ans++;
}
}
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
- 时间
:两遍 DFS 各访问每点每边常数次,出度统计再扫 条边。与 C++ 正解(Kosaraju / Tarjan)同阶。 - 空间
:正图、反图邻接表与若干 数组。
总结
「被全员认可的点」是传递闭包查询,逐点求可达性是
验证:仓库真实数据 data/popular1..13 全部 PASS(verify.sh,13/13,最长 0.034s / 峰值 25.1MB)。