形式化题目

给定 nn 个点和若干有向边:第 ii 个点指向 jj 表示 ii 允许把资料传给 jj。资料放在某个点上后会沿有向边自动传播,一张光盘覆盖放置点可达的全部点(含放置点自身)。求让所有点都获得资料所需的最少光盘数。

正解

思路

第一步:把"拷贝"翻译成可达性。 “若 AA 愿意给 BB、BB 愿意给 CC,则 AA 拿到后 BB、CC 都能拿到”——这正是有向图上路径的定义。所以一张光盘放在 pp 手里,最终覆盖的恰好是 pp 的可达集 reach(p)\mathrm{reach}(p)(含 pp 自己)。问题变成:选最少的起点,让它们的可达集之并盖住所有点。

第二步:找到"必须直接拿光盘"的点。 强连通分量(SCC)内部互相可达:光盘放在分量内任何一人手里,覆盖效果完全相同。把每个 SCC 缩成一个点,得到一个 DAG。入度为 00 的分量(源分量)拿不到任何人传来的资料——若外部点能把资料传进源分量 SS,传进来的最后一条边就是一条从其他分量指向 SS 的边,与 SS 入度为 00 矛盾。因此:

  • 下界:每个源分量必须有人直接持有光盘;不同源分量互相不可达,一张光盘至多覆盖一个源分量,故至少需要(源分量个数)张。
  • 上界:DAG 中任何分量沿边倒退走,总能到达某个入度为 00 的分量;给每个源分量各放一张,资料顺流而下覆盖全部分量,故(源分量个数)张足够。

答案 = 缩点 DAG 中入度为 00 的强连通分量个数。 以样例为例(边 1→2,1→3,1→4,2→4,2→5,5→11\to2,1\to3,1\to4,2\to4,2\to5,5\to1):

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}"]

1→2→5→11\to2\to5\to1 构成环,把 1,2,51,2,5 缩成源分量 AA;BB、CC 都有来自 AA 的入边,只有 AA 入度为 00。在 AA 中任选一人(如 1 号)放光盘,11 能到 {2,3,4,5}\{2,3,4,5\},全覆盖,答案为 11。

第三步:用 bitset 传递闭包数出源分量。 n⩽200n \leqslant 200,不必写 Tarjan;传递闭包配 Python 大整数(天然 bitset)反而更短更快:

  • reach[i]:ii 能到达的点集(显式并入自环)。
  • Floyd 式闭包:若 reach[i] 第 kk 位为 1,则整行 reach[i] |= reach[k]。
  • pred[i](能到达 ii 的点集)就是闭包矩阵的第 ii 列。
  • 强连通分量即互相可达的点集:SCC(i)=reach[i]&pred[i]\mathrm{SCC}(i) = \mathrm{reach}[i] \mathbin{\&} \mathrm{pred}[i](两个集合都含 ii 自身)。

判定"源"只需要一个位运算:

SCC(i)\mathrm{SCC}(i) 是源   ⟺  pred[i]⊆reach[i]\iff \mathrm{pred}[i] \subseteq \mathrm{reach}[i],即"所有能到 ii 的点,ii 反过来也能到"。

若 SCC(i)\mathrm{SCC}(i) 非源,必有外部点 jj 能到 ii 而 ii 到不了 jj(jj 进入该分量的路径存在,ii 却无法反向出去),此时 pred[i] & ~reach[i] 非空;反之,能到 ii 的点全部落在 SCC(i)\mathrm{SCC}(i) 内,说明没有任何外部路径能进入该分量,它是源。

对样例逐人检查(集合均含自环):

ii reach[i]\mathrm{reach}[i](ii 能到谁) pred[i]\mathrm{pred}[i](谁能到 ii) SCC(i)\mathrm{SCC}(i) pred[i]⊆reach[i]\mathrm{pred}[i] \subseteq \mathrm{reach}[i]?
1 {1,2,3,4,5}\{1,2,3,4,5\} {1,2,5}\{1,2,5\} {1,2,5}\{1,2,5\} 是 → 源
2 {1,2,3,4,5}\{1,2,3,4,5\} {1,2,5}\{1,2,5\} {1,2,5}\{1,2,5\} 是 → 源
3 {3}\{3\} {1,2,3,5}\{1,2,3,5\} {3}\{3\} 否(1,2,51,2,5 能到 33,33 到不了他们)
4 {4}\{4\} {1,2,4,5}\{1,2,4,5\} {4}\{4\} 否(1,2,51,2,5 能到 44,44 到不了他们)
5 {1,2,3,4,5}\{1,2,3,4,5\} {1,2,5}\{1,2,5\} {1,2,5}\{1,2,5\} 是 → 源

同一源分量的所有成员都通过检查且 SCC\mathrm{SCC} 掩码相同,非源分量的成员全部不通过,所以把通过者的 reach[i] & pred[i] 放进集合并计数,得到的就是源分量个数——样例去重后只剩 {1,2,5}\{1,2,5\},输出 11。

代码

Python 版:

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++ 版(同一算法):

cpp
/**
 * 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;
}

复杂度

  • 时间:闭包为 nn 轮 Floyd,每轮 nn 次大整数 OR,每次 O(n/w)O(n/w)(ww 为位密度因子,CPython 约 30),总计 O(n3/w)O(n^3/w);取列与源判定各 O(n2)O(n^2)。n=200n = 200 时约几万次字长级运算,实测单组数据约 0.02s,无 TLE 风险。这与本题 O(n3)O(n^3) 的 Floyd 闭包经典实现同阶;若数据规模更大,应改用 Tarjan/Kosaraju 求 SCC 后统计入度,O(n+m)O(n+m)。
  • 空间:闭包矩阵 O(n2/w)O(n^2/w),约 5KB,远低于 128MB。

总结

  • 题眼:一张光盘的覆盖范围 = 放置者的可达集;"无法被间接覆盖"的点恰好构成缩点 DAG 的源分量,最少光盘数 = 源分量个数。
  • 判定技巧:bitset 闭包下,SCC(i)=reach[i]&pred[i]\mathrm{SCC}(i) = \mathrm{reach}[i] \mathbin{\&} \mathrm{pred}[i],"是否源"化为一次 pred[i]⊆reach[i]\mathrm{pred}[i] \subseteq \mathrm{reach}[i] 判断,去重计数即答案。
  • 同模型(缩点后数入度 0 分量)是图论常见套路:校园网络选服务器、Popular Cows 等都是它的变体;规模大时换 Tarjan/Kosaraju。