0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 B3644,日期见页头。两边不一致时信原站。
题目描述
有个人的家族很大,辈分关系很混乱,请你帮整理一下这种关系。给出每个人的后代的信息。 输出一个序列,使得每个人的后辈都比那个人后列出。
输入格式
第 1 行一个整数 N(1 ≤ N ≤ 100),表示家族的人数。
接下来 N 行,第 i 行描述第 i 个人的后代编号 aᵢ,ⱼ,表示 aᵢ,ⱼ 是 i 的后代。
每行最后是 0 表示描述完毕。
输出格式
输出一个序列,使得每个人的后辈都比那个人后列出。 如果有多种不同的序列,输出任意一种即可。
输入输出样例
输入
5 0 4 5 1 0 1 0 5 3 0 3 0
输出
2 4 5 3 1
边是「长辈 → 后辈」:2→4、2→5、2→1、3→1、4→5、4→3、5→3。
只有 2 号没有长辈,所以它第一个;接着 4、5、3、1。
⚠⚠ 注意输出格式那句「输出任意一种即可」—— 这是本页的全部难点, 而且第 ① 步会发现:这道题「答案不唯一」这件事,在这组样例上根本看不出来。
1★ 正解:本章的 Kahn 原样搬过来,只换了读入
题面那句「第 i 行是第 i 个人的后代」+「后辈要后列出」⇒ 边是 i → j(长辈指向后辈),
求任意一个拓扑序。
// ★ B3644 正解:Kahn 算法(入度 + 队列)—— 本章 fast.cpp 原样搬过来,只换了读入。//// 题面:给每个人的**后代**列表(以 0 结尾),要求「每个人的后辈都比那个人后列出」// ⇒ 一条「i 是 j 的长辈」的关系就是一条有向边 `i → j` ⇒ 求任意一个拓扑序。//// ⚠⚠ 这道题**答案不唯一**(题面明写「如果有多种不同的序列,输出任意一种即可」)——// 于是对拍**不能逐字节比**。本页的办法是写一个**验证器**(b3644Check.cpp),// 只问「你给的这个顺序合不合法」。解析页第 ③ 步说了这件事值多少。//// ⚠ 读入是**变长行**:`后代… 0`。和 [P1113](/sol/p1113/) 一样,别去管换行,// 直接把整个输入当一条数字流读到 0 为止。//// 复杂度 O(n + m)。题面 n ≤ 100,随便写。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n; if (!(cin >> n)) return 0; vector<vector<int>> g(n + 1); vector<int> indeg(n + 1, 0); for (int i = 1; i <= n; i++) { int v; while (cin >> v && v != 0) { g[i].push_back(v); indeg[v]++; } }
queue<int> q; for (int i = 1; i <= n; i++) if (!indeg[i]) q.push(i); // ★ 所有入度为 0 的点,一个都不能少 string out; while (!q.empty()) { int u = q.front(); q.pop(); if (!out.empty()) out += ' '; out += to_string(u); for (int v : g[u]) if (--indeg[v] == 0) q.push(v); } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
⚠ 读入是变长行(后代… 0)。和上一轮 P1113 一样:别去管换行,
cin >> 本来就跳过所有空白,把整个输入当一条数字流读到 0 为止。
2⚠⚠ 「输出任意一种即可」—— 于是对拍不能逐字节比,得先写一个裁判
它只判两件事,逐字照题面,不假设任何算法:
① 输出是 1..n 的一个排列(不重不漏);② 每一条边,长辈都排在后辈前面。
它只能证明「这个答案合法」,不能证明「答案存在时你没漏报」。
一份什么都不输出的程序过不了第 ①(长度不对),
但一份「输出了合法序、却漏掉判环」的程序,它看不出来 ——
这和第 20 章那句「对拍只能证伪」是同一件事的另一面。
(本章正文 check.cpp 里也写着这一条。)
3★★★ 三种正确写法:都合法,而逐字节比会报 283 轮「不一致」
同一个 Kahn,把队列换成栈;再写一份完全不同的 DFS 逆后序:
// ⚠ B3644:把 Kahn 里的**队列换成栈** —— 看着像 bug,其实**一次都不会错**。//// 「先进先出」换成「后进先出」,输出的顺序当场就变了(样例上 `2 4 5 3 1` 变成别的)。// 可拓扑排序**从来没要求过顺序**:Kahn 的正确性只依赖一件事 ——// **一个点被输出时,它的所有前驱都已经输出过了**。而「从哪个容器里挑下一个」// 对这件事毫无影响,挑谁都行。//// ⇒ ★★ 所以这一版**答案不同、但同样合法**。300 轮验证器全过(解析页第 ③ 步)。// ⚠ 而如果拿它去和正解**逐字节比**,会报一大堆「不一致」—— 那全是假阳性。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<vector<int>> g(n + 1); vector<int> indeg(n + 1, 0); for (int i = 1; i <= n; i++) { int v; while (cin >> v && v != 0) { g[i].push_back(v); indeg[v]++; } } vector<int> st; for (int i = 1; i <= n; i++) if (!indeg[i]) st.push_back(i); string out; while (!st.empty()) { int u = st.back(); st.pop_back(); // ★ 栈:后进先出 if (!out.empty()) out += ' '; out += to_string(u); for (int v : g[u]) if (--indeg[v] == 0) st.push_back(v); } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
| 300 轮随机 DAG | |
|---|---|
| 队列版 / 栈版 / DFS 版 过验证器 | ★ 300 / 300 / 300 |
| ⚠ 逐字节比:栈版和队列版不同的轮数 | ★ 283 |
| ⚠ 逐字节比:DFS 版和队列版不同的轮数 | 282 |
| ★ 而真正有多解(拓扑序不唯一)的轮数 | ★ 283 —— 一个不差 |
⇒ ★★★ 拿逐字节对拍去验这道题,300 轮里会报 283 轮「不一致」,而真正的错误是 0 轮。 94% 的报警全是假的。
而那个「一个不差」是能证的:只要某一步同时有两个入度为 0 的点, 队列取最先进来的、栈取最后进来的 —— 必然分岔。 (DFS 版差的那 1 轮,是它恰好和队列版走出了同一个串。)
★★ 对照本书前两次量到的同一件事: 第 26 章 P1040 是「多解 80 轮,可两版 tie-break 恰好一致 ⇒ 0 轮不同」, 第 27 章 P3478 是「22 轮假阳性」—— 这一页是同一条线上最极端的一点:283 / 300。
生成器换成 1 → 2 → … → n 一条链:拓扑序唯一 300 / 300 轮,
三种写法输出相同 300 / 300 轮。
⇒ 「逐字节比」不是永远错的,它只在「答案唯一」的时候才对 —— 而这道题的题面明明白白写着「输出任意一种即可」。 ⇒ ★ 看到题面写「任意一种 / 输出任意解」,第一件事是去写验证器,不是去写对拍。
4⚠ 两个错法 —— 而它们各配一条精确的恒等式
// ✗ B3644 错法一:**边的方向存反了** —— 把「i 的后代是 j」读成了「j → i」。//// 题面那句话要一个字一个字读:「第 i 行描述第 i 个人的**后代**编号」,// 而输出要求「每个人的**后辈**都比那个人**后**列出」⇒ 边是 `i → j`(长辈指向后辈)。//// ★ 说清楚它算了什么:它输出的是**反着的**拓扑序(后辈在前)。// ⚠ 而这个错法在**每条边上**都错,所以官方样例一测就死。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<vector<int>> g(n + 1); vector<int> indeg(n + 1, 0); for (int i = 1; i <= n; i++) { int v; while (cin >> v && v != 0) { g[v].push_back(i); indeg[i]++; } // ★ 反了 } queue<int> q; for (int i = 1; i <= n; i++) if (!indeg[i]) q.push(i); string out; while (!q.empty()) { int u = q.front(); q.pop(); if (!out.empty()) out += ' '; out += to_string(u); for (int v : g[u]) if (--indeg[v] == 0) q.push(v); } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
// ✗ B3644 错法二:**一开始只把 1 号点入队**(忘了「所有入度为 0 的点」)。//// Kahn 的第一步是「把**所有**入度为 0 的点放进队列」。只放一个,// 那些不在 1 号点可达范围里的点就永远出不来 ⇒ **输出的序列短了一截**。//// ⚠ 它和第 30 章 [B3625](/sol/b3625/) 第一版那个「只从 1 号点搜」是同一个形状的错 ——// **默认「图是从 1 号点连通的」**。题面从来没这么保证过。// ★ 而这一次验证器能一眼看穿它:长度不是 n。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<vector<int>> g(n + 1); vector<int> indeg(n + 1, 0); for (int i = 1; i <= n; i++) { int v; while (cin >> v && v != 0) { g[i].push_back(v); indeg[v]++; } } queue<int> q; if (indeg[1] == 0) q.push(1); // ★ 只放 1 号 string out; while (!q.empty()) { int u = q.front(); q.pop(); if (!out.empty()) out += ' '; out += to_string(u); for (int v : g[u]) if (--indeg[v] == 0) q.push(v); } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
| 300 轮 | 被验证器抓 | 而它 ≡ |
|---|---|---|
| 边的方向存反了 | 249 | ★ 图里真的有边的轮数 = 249(一个不差) |
| 只把 1 号点入队 | 287 | ★ 其中 287 轮是「长度就不够」被抓的 |
- 存反了:只要有一条边
u → v,反向之后v就必然排在u前面 ⇒ 必被抓。 漏掉的 51 轮,正是生成器造出零条边的那 51 轮(那时任意排列都合法)。 - 只放 1 号点:Kahn 的第一步是「把所有入度为 0 的点放进队列」。只放一个, 不在 1 号可达范围里的点就永远出不来 ⇒ 输出短了一截,验证器第 ① 条就拦住了。 ⚠ 它和第 30 章 B3625 第一版是同一个形状的错:默认「图是从 1 号点连通的」。
5★ 一件容易忽略的事:官方样例把这一页的主题整个盖住了
三种正确写法在官方样例上打出的是同一个串 2 4 5 3 1:
| 队列 | 栈 | DFS 逆后序 | |
|---|---|---|---|
| 官方样例 | 2 4 5 3 1 |
2 4 5 3 1 |
2 4 5 3 1 |
| 300 轮随机 DAG 上和队列版不同 | —— | 283 | 282 |
⇒ ★★ 这组样例里只有 2 号点没有长辈,接下来每一步也都只有一个候选 —— 它恰好是一个「拓扑序唯一」的例子。 ⇒ 于是「答案不唯一、不能逐字节比」这件全页最要紧的事,只看样例是发现不了的。 ★ 又一次那条老规矩的现场:官方样例给了几组就跑几组,但别指望它替你把题面读完。
6度量程序和生成器
7一页纸
| ★ 关键的一步 | 「后代」= 边 i → j ⇒ 本章 Kahn 原样搬过来(n ≤ 100,随便跑) |
| ★★★ 全页最要紧的一条 | 题面写「输出任意一种即可」⇒ 先写验证器,别写逐字节对拍 |
| ★★★ 它值多少 | 三种写法都合法(300/300/300),可逐字节比报 283 轮不一致 —— 94% 是假阳性 |
| ★ 而 283 ≡ 283 | 假阳性轮数恰好等于「拓扑序不唯一」的轮数(能证:两个候选时队列和栈必分岔) |
| ★ 对照档 | 换成一条链 ⇒ 唯一 300/300、三版输出相同 300/300 ⇒ 逐字节比只在答案唯一时才对 |
| 两个错法 | 存反了被抓 249 ≡ 有边的轮数;只放 1 号点被抓 287(长度就不够) |
| ⚠ 官方样例 | 三种正确写法打出同一个串 —— 这道题的「多解」在样例上根本看不出来 |