把“愿意拷贝”建成有向图,一张光盘覆盖放置者可达的所有人;bitset 传递闭包求 SCC,能到 i 的点都能被 i 反达即源分量,答案就是源分量个数。
OJ: roj
题目 ID: 1383
难度:普及+/提高-
标签:图论强连通分量传递闭包位运算python
创建: 2026-09-30 08:10
更新: 2026-10-07 12:15
形式化题目
给定
正解
思路
第一步:把"拷贝"翻译成可达性。 “若
第二步:找到"必须直接拿光盘"的点。 强连通分量(SCC)内部互相可达:光盘放在分量内任何一人手里,覆盖效果完全相同。把每个 SCC 缩成一个点,得到一个 DAG。入度为
- 下界:每个源分量必须有人直接持有光盘;不同源分量互相不可达,一张光盘至多覆盖一个源分量,故至少需要(源分量个数)张。
- 上界:DAG 中任何分量沿边倒退走,总能到达某个入度为
的分量;给每个源分量各放一张,资料顺流而下覆盖全部分量,故(源分量个数)张足够。
答案 = 缩点 DAG 中入度为
graph LR
subgraph S1["分量 A = {1, 2, 5}"]
a1["1"] --> a2["2"]
a2 --> a5["5"]
a5 --> a1
end
S1 -->|"1→3"| s3["分量 B = {3}"]
S1 -->|"1→4、2→4"| s4["分量 C = {4}"]
第三步:用 bitset 传递闭包数出源分量。
reach[i]:能到达的点集(显式并入自环)。 - Floyd 式闭包:若
reach[i]第位为 1,则整行 reach[i] |= reach[k]。 pred[i](能到达的点集)就是闭包矩阵的第 列。 - 强连通分量即互相可达的点集:
(两个集合都含 自身)。
判定"源"只需要一个位运算:
是源 ,即"所有能到 的点, 反过来也能到"。
若 pred[i] & ~reach[i] 非空;反之,能到
对样例逐人检查(集合均含自环):
| 1 | 是 → 源 | |||
| 2 | 是 → 源 | |||
| 3 | 否( |
|||
| 4 | 否( |
|||
| 5 | 是 → 源 |
同一源分量的所有成员都通过检查且 reach[i] & pred[i] 放进集合并计数,得到的就是源分量个数——样例去重后只剩
代码
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 08:10
# update_at: 2026-09-30 08:10
import sys
def transitive_closure(adj: list[int]) -> list[int]:
"""bitset 版 Floyd 传递闭包:reach[i] = i 能到达的点集(含自己)的位掩码。"""
reach = adj[:]
for k in range(len(reach)):
rk = reach[k]
# i 能到 k,就能到 k 能到的一切:一次大整数 OR 完成整行转移
reach = [r | rk if r >> k & 1 else r for r in reach]
return reach
def count_source_sccs(reach: list[int], pred: list[int]) -> int:
"""统计"源"强连通分量个数:能到 i 的点都能被 i 反向到达,说明没有外部边进入 i 所在分量。"""
return len({reach[i] & pred[i] for i in range(len(reach)) if not pred[i] & ~reach[i]})
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
adj: list[int] = []
for i in range(n):
bits = 1 << i # 自己算进可达集,闭包与 SCC 判断都依赖这一点
while j := next(data): # 每行以 0 结尾
bits |= 1 << (j - 1)
adj.append(bits)
reach = transitive_closure(adj)
pred = [sum(1 << j for j in range(n) if reach[j] >> i & 1) for i in range(n)] # reach 的第 i 列
print(count_source_sccs(reach, pred))
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 00:20
* update_at: 2026-10-05 00:20
*/
#include <cstdio>
#include <bitset>
using namespace std;
typedef long long ll;
const int MAXN = 205;
// 题目含义:第 i 个营员愿意把资料拷贝给他这一行给出的营员。
// 一张光盘放在 p 手里,能覆盖的正是 p 在有向图上可达的所有点(含 p 自己)。
// 答案 = 缩点后有向无环图中入度为 0 的强连通分量(源分量)个数。
bitset<MAXN> reach[MAXN]; // reach[i] = i 能到达的点集(含 i 自己)的位掩码
bitset<MAXN> pred[MAXN]; // pred[i] = 能到达 i 的点集的位掩码,即闭包矩阵的第 i 列
char counted[MAXN]; // counted[i] = 1 表示 i 所在的源分量已经统计过,用于去重
int main() {
ll n;
scanf("%lld", &n);
for (ll i = 1; i <= n; i++) {
reach[i].set(i); // 自己也算可达,闭包与 SCC 判定都依赖这一点
ll j;
scanf("%lld", &j);
while (j != 0) { // 每行以 0 结束
reach[i].set(j);
scanf("%lld", &j);
}
}
// Floyd 式传递闭包:i 能到 k,就能到 k 能到的一切
for (ll k = 1; k <= n; k++) {
for (ll i = 1; i <= n; i++) {
if (reach[i].test(k)) {
reach[i] |= reach[k];
}
}
}
// 转置闭包矩阵:reach[j] 的第 i 位为 1,说明 j 能到 i,即 j 属于 pred[i]
for (ll j = 1; j <= n; j++) {
for (ll i = 1; i <= n; i++) {
if (reach[j].test(i)) {
pred[i].set(j);
}
}
}
// 统计源分量个数:SCC(i) = reach[i] & pred[i]。
// pred[i] 中所有点都能被 i 反过来到达(pred[i] 是 reach[i] 的子集)时,
// 没有任何外部边能进入 i 所在分量,它就是源分量;同一分量去重只算一次。
ll ans = 0;
for (ll i = 1; i <= n; i++) {
if (counted[i]) {
continue;
}
if ((pred[i] & ~reach[i]).none()) {
ans++;
bitset<MAXN> scc = reach[i] & pred[i];
for (ll j = 1; j <= n; j++) {
if (scc.test(j)) {
counted[j] = 1;
}
}
}
}
printf("%lld\n", ans);
return 0;
}复杂度
- 时间:闭包为
轮 Floyd,每轮 次大整数 OR,每次 ( 为位密度因子,CPython 约 30),总计 ;取列与源判定各 。 时约几万次字长级运算,实测单组数据约 0.02s,无 TLE 风险。这与本题 的 Floyd 闭包经典实现同阶;若数据规模更大,应改用 Tarjan/Kosaraju 求 SCC 后统计入度, 。 - 空间:闭包矩阵
,约 5KB,远低于 128MB。
总结
- 题眼:一张光盘的覆盖范围 = 放置者的可达集;"无法被间接覆盖"的点恰好构成缩点 DAG 的源分量,最少光盘数 = 源分量个数。
- 判定技巧:bitset 闭包下,
,"是否源"化为一次 判断,去重计数即答案。 - 同模型(缩点后数入度 0 分量)是图论常见套路:校园网络选服务器、Popular Cows 等都是它的变体;规模大时换 Tarjan/Kosaraju。