Codeforces 强连通分量三题 从互相可达到缩点后的源与汇
拓扑排序擅长处理没有环的依赖图。但是,遇到环不一定意味着问题无解:有时环表示一组必须一起考虑的顶点。先把“互相可达”的点压成一个整体,再研究整体之间的单向关系,就能把一般有向图变成 DAG。
本篇依次解决三个问题:每个整体内部怎样选点、怎样让首都覆盖所有整体、怎样找到不会牵连外部的最小维护集合。建议已了解 DFS、邻接表和有向可达性后阅读;不需要先背 Tarjan 模板。
三题路线与官方信息
| 题目 | 官方难度 | 官方标签 | 本篇训练重点 |
|---|---|---|---|
| 427C Checkposts | 1700 | dfs and similar、graphs、two pointers | 分量内最小费用与方案乘法 |
| 999E Reachability from the Capital | 2000 | dfs and similar、graphs、greedy | 未覆盖 DAG 的源分量 |
| 949C Data Center Maintenance | 1900 | dfs and similar、graphs | 从冲突条件建蕴含边,选择汇分量 |
题名、难度、标签与下文约束于 2026-10-02 核对官方题页。官方标签原样保留;例如 427C 虽带 two pointers 标签,本篇解法并不使用双指针。三题不是机械递增的难度阶梯,而是同一结构的三种用途。
| 题目 | 规模与数据范围 | 时空限制 |
|---|---|---|
| 427C | 1 ≤ n ≤ 100000,0 ≤ m ≤ 300000,0 ≤ 费用 ≤ 10⁹ | 2 秒,256 MB |
| 999E | 1 ≤ n ≤ 5000,0 ≤ m ≤ 5000,1 ≤ s ≤ n | 2 秒,256 MB |
| 949C | 2 ≤ n ≤ 100000,1 ≤ m ≤ 100000,2 ≤ h ≤ 100000,0 ≤ uᵢ < h | 1 秒,512 MB |
前两题的有向边没有自环、没有同方向重复边;949C 的客户连接两个不同数据中心,初始维护小时不同,但客户对可以重复。以下代码不依赖边去重。
从互相可达到缩点
如果 a 能走到 b,b 也能走到 a,就把它们视为同一类。这是等价关系:自身可达、互相对称,而且路径可以拼接,所以具有传递性。每个等价类就是一个强连通分量,简称 SCC。只有 a 能到 b 不够;无向并查集不能直接替代这个判定。
将每个 SCC 压成一个点,仅保留跨分量边,得到缩点图。它必然无环:若几个分量构成环,沿环就能相互到达,它们本应是同一个分量。
flowchart TD A["原图:1 与 2 互相可达"] --> B["压成分量 A"] C["原图:3 与 4 互相可达"] --> D["压成分量 B"] B -->|"原图存在 2 → 3"| D D --> E["分量 C:单独的点 5"]
| 层次 | 能够忽略什么 | 仍要保留什么 |
|---|---|---|
| 分量内部 | 具体从哪个点进入,内部都能到达 | 点的费用、个数等权重 |
| 分量之间 | 同一方向的重复边通常不影响可达性 | 边的方向 |
| DAG 上 | 不再需要处理有向环 | 源、汇、可达范围等全局关系 |
一个容易混淆的地方是:SCC 并不等于“度数大的一团点”,也不要求任意两点之间都有直接边。一条有向大环就是一个 SCC。
非递归 Kosaraju 为什么要记录退出顺序
Kosaraju 做两遍搜索。第一遍在原图建立完整 DFS 森林,在一个点的邻居全部处理完后,才把它加入退出序列。第二遍按退出序列的逆序,在反图中搜索,每次新搜索得到一个分量。
为什么方向恰好匹配?对缩点边 A → B,两分量不可能彼此可达。若先访问 A,搜索会完成 B 再完成 A;若先访问 B,B 无法进入 A,仍会先完成。因此 A 的最大退出时刻大于 B 的最大退出时刻。尚未分配的分量中,退出最晚者是原缩点图的源;转到反图后,它不会走进另一个尚未分配的分量。重复剥离便得到全部 SCC。
| 第一遍栈的状态 | 含义 | 何时改变 |
|---|---|---|
| seen[v] | v 是否已经入过栈 | 入栈时标记 |
| next[v] | 下一个待检查的邻居下标 | 每检查一条边就增加 |
| st.back() | 当前尚未退出的 DFS 节点 | 模拟递归调用栈 |
| order | 已完成节点的退出序列 | 邻居全部处理完才追加 |
flowchart TD
A["栈顶节点 v"] --> B{"还有未检查的邻居?"}
B -->|"有"| C["取下一条边并推进游标"]
C --> D{"邻居是否未访问?"}
D -->|"是"| E["标记邻居并入栈"]
D -->|"否"| A
E --> A
B -->|"没有"| F["把 v 加入退出序列并出栈"]
例如只有 1 → 2 → 3 时,第一遍从 1 开始得到退出序列 [3,2,1];反向处理 [1,2,3],在反图中依次得到三个单点分量。如果误把入栈序列 [1,2,3] 当退出序列,反向从 3 搜索会把三个点错误合并。
第二遍只需收集反图可达的未分配点,不需要再记录退出时刻,可以用普通栈。以下三份实现都使用显式栈,避免十万点长链造成递归栈溢出;每个 SCC 对象只调用一次 build。
第一题 427C 每个分量选一个最低费用点
题目的保护关系要求双向可达:在 i 建检查站,能够保护 j 的条件是 i=j,或 i 能到 j 且 j 能到 i。于是一个站恰好能覆盖自身 SCC,不能覆盖其他 SCC。
先最小费用,再最少站点
每个分量至少需要一个站,而任意一个站都足够。因为费用非负,选最低费用点不会更差。特别注意:费用允许为 0。即使多个零费用站不增加总费用,题目还要求在最低费用下尽量少建站,因此仍然每个分量恰好选一个。
| 分量 | 点费用 | 最低贡献 | 可选最低费用点数 |
|---|---|---|---|
| A | 4、1、1 | 1 | 2 |
| B | 0、0 | 0 | 2 |
| C | 7 | 7 | 1 |
| 合并结果 | 各分量独立 | 8 | 2 × 2 × 1 = 4 |
正确性由两部分组成:任何方案在每个分量的花费都不小于分量最低费用;逐个选一个最低费用点达到该下界,且站点数也是必要的分量数。每个分量的选择互不限制,所以方案数相乘,对 1000000007 取模。
| 朴素方案 | 问题 | SCC 后的处理 |
|---|---|---|
| 从每点分别搜索双向可达集合 | 可达性重复计算,最坏 O(n(n+m)) | 两遍 DFS 一次划分 |
| 枚举所有站点子集 | 2ⁿ 个方案 | 每个分量扫描一次 |
| 只看最低费用不看站点数 | 多选零费用点导致多计方案 | 每个分量恰好一个站 |
总费用最大为 10⁵ × 10⁹ = 10¹⁴,不能使用 int。方案数的乘法中间值也使用 long long。
完整 C++17 实现
1 |
|
第二题 999E 未覆盖子图的源各需要一条新路
要求添加尽量少的有向边,使首都 s 能到达所有城市。先缩点,再从 s 所在分量搜索,将已经能到达的分量标记。剩下的分量诱导出一个 DAG,答案就是这个子图中入度为零的分量数。
为什么不数剩余城市或原图零入度点
假设首都已经覆盖 R,剩下 A、B、C、D,边为 A → C、B → C、C → D。给 A 和 B 各加一条从首都出发的边就足够,答案是 2,不是剩余分量数 4。
flowchart TD S["首都所在分量 R"] -. "新增" .-> A["未覆盖源 A"] S -. "新增" .-> B["未覆盖源 B"] A --> C["未覆盖分量 C"] B --> C C --> D["未覆盖分量 D"]
| 分量 | 未覆盖子图中的入边来源 | 是否需要直接接入 |
|---|---|---|
| A | 无 | 是 |
| B | 无 | 是 |
| C | A、B | 否 |
| D | C | 否 |
下界:每个未覆盖源都必须有新边进入,否则从首都无法第一次走入它。不同源不能共用同一条进入边,因此至少需要源的数量条边。注意原图不会存在“已覆盖分量 → 未覆盖分量”的边,否则后者早已被搜索覆盖。
上界:从首都分别连到每个未覆盖源。在有限 DAG 中,任一点都能沿入边反向追溯到一个源,因此这些新边能覆盖全部剩余分量。上下界相等,结论成立。
| 错误计数 | 反例 |
|---|---|
| 数未覆盖顶点 | 一个不可达的有向环只需要一条新路 |
| 数原图零入度顶点 | 上述环的每点入度都非零,仍需新路 |
| 数全部缩点图的源 | 首都已经覆盖的部分不应再次收费 |
| 数所有未覆盖分量 | A → B → C 只需给 A 一条路 |
实现只记录 hasIn 布尔值,不必计算准确入度或给平行边去重。这里的 DAG 用于可达性与源判断,不需要真正运行拓扑排序。
完整 C++17 实现
1 |
|
第三题 949C 最小维护集合是一个汇分量
每个数据中心每天在小时 uᵢ 维护,时间按 h 小时循环。每位客户连接两个中心,初始两者的维护小时不同。现在要选择非空集合,将所选中心的维护时间都加一并对 h 取模,仍让每位客户的两个中心错开。目标是选择尽量少的中心。
从一次冲突推出边的方向
考虑客户连接 a、b。只有其中一个被选中时,才可能把原来不同的小时变成相同。
| 选择 a | 选择 b | 是否可能新冲突 |
|---|---|---|
| 否 | 否 | 不变,合法 |
| 是 | 是 | 同时循环平移,仍然不同 |
| 是 | 否 | 当 (uₐ+1) mod h = uᵦ 时冲突 |
| 否 | 是 | 当 (uᵦ+1) mod h = uₐ 时冲突 |
因此,如果 (uₐ+1) mod h = uᵦ,就建边 a → b,意思是“选择 a 必须选择 b”。反向条件同理。两个条件必须写成独立的 if:h=2 时它们能同时成立,不能用 else if 漏掉一个方向。
把每条边理解为蕴含关系后,合法选择集恰好是对后继封闭的非空集合:选一个点就必须选它能到达的所有点。一个 SCC 内的点必须同选或同不选。
flowchart TD A["只调整 a 会与 b 撞时"] --> B["建立蕴含边 a → b"] B --> C["强连通分量必须整体选择"] C --> D["跨分量边表示继续牵连其他分量"] D --> E["无出边的汇分量可以单独选择"] E --> F["取顶点数最少的汇分量"]
最优性不只是“没有出边就合法”
一个汇分量没有跨分量出边,因此单独选择它确实合法。但合法还不等于最优,需要补上下界。
任取非空合法集合,从其中任一分量沿 DAG 出边不断向前。因为集合对后继封闭,经过的所有分量都在集合里;又因为 DAG 有限,最终到达整个缩点图的一个汇。该合法集合至少包含这个汇分量的全部点,其大小不小于“所有汇分量中的最小大小”。选最小汇恰好达到下界。
| h 与小时 | 客户连接 | 蕴含图 | 最小合法选择 |
|---|---|---|---|
| h=5,0、1、2 | 1—2、2—3 | 1 → 2 → 3 | 只选 3 |
| h=2,0、1 | 1—2 | 1 ↔ 2 | 两点都选 |
| h=3,0、1、2 | 1—2、2—3、3—1 | 三点成环 | 三点都选 |
| h=5,4、0 | 1—2 | 1 → 2 | 只选 2 |
最后一行专门检查跨过午夜的取模。若存在孤立点,它自己就是大小 1 的汇分量,可以直接构成最优解。输入客户对重复只会产生重复蕴含边,不影响证明。
完整 C++17 实现
1 |
|
复杂度与实现边界
| 部分 | 时间 | 额外空间 | 需要留意 |
|---|---|---|---|
| 非递归 Kosaraju | O(n+m) | O(n+m) | 原图、反图和显式 DFS 栈 |
| 427C 分量统计 | O(n) | O(n) | 费用与乘法使用 64 位 |
| 999E 缩点与覆盖 | O(n+m) | O(n+m) | 仅统计未覆盖部分的入边 |
| 949C 蕴含图与汇判断 | O(n+m) | O(n+m) | 每个客户至多生成两条边 |
表中 949C 的 m 指客户数,最多 2m 条有向蕴含边;常数变化不改变线性复杂度。算法线性不代表可以忽略语言、内存分配与评测机差异,本地运行不等于官方提交通过。
| 错误清单 | 修正 |
|---|---|
| 只从节点 1 开始第一遍 DFS | 必须遍历所有未访问根,覆盖非连通部分 |
| 第一遍记录入栈顺序 | 保存所有邻居处理完后的退出顺序 |
| 第二遍仍用原图 | 必须在反图搜索 |
| 十万点链使用未经评估的递归 | 显式栈模拟退出过程 |
| 把分量编号当可靠业务顺序 | 按实际跨分量边判断入度、出度 |
| 949C 随便取最小 SCC | 必须在汇分量中取最小,否则会牵连外部 |
| 949C 忘记非空限制 | 输出至少一个点,空集合不是答案 |
| 427C 将零费用点随意多选 | 最低费用之下还需最少站点数 |
用独立方法验证而不是复写同一模板
验证脚本为 tests/verify-scc-three-article.cjs,提取本文三份代码直接编译。小数据基准不调用 SCC:427C 用传递闭包和所有站点子集检查费用、站点数及方案数;999E 枚举从首都补边的目标集合并做 BFS;949C 枚举非空集合,实际平移小时并逐客户检查冲突。
999E 的基准为何可以只枚举首都出发的边?对任意可行补边方案,把每条新边 a → b 改为 s → b,原来沿新边到达 b 的能力不会减弱;之后的旧边路径仍然可走。因此总有同样数量以内的首都出边方案,不会漏掉更优解。这个基准虽慢,却不依赖缩点或源数量定理。
此外覆盖空图、全零费用、长链、整图一个 SCC、重复客户、h=2 双向蕴含、循环边界和最大规模。验证只说明这些测试范围内与独立基准一致,不替代正确性证明。
本轮三份 C++17 均无警告编译,共通过 1,166 次执行与 31,880 个独立子集核验;小数据包含 199 张有向图和 550 组维护安排,另有官方样例及十万点、三十万条边等边界测试。
下一步怎样串回路线
先把 拓扑排序三题里的 DAG 依赖关系重新看一遍,再问“如果出现环,环代表矛盾还是一个必须整体处理的集合”。随后比较 并查集三题:并查集合并的是无向连通性,而 SCC 利用的是有向双向可达,两者不能混用。
本篇最值得保留的不是模板名字,而是三个不同的问题出口:分量内部做统计、未覆盖子图找源、后继封闭集合找汇。返回算法路线可以按图论与树上查询继续练习。