0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P5318,日期见页头。两边不一致时信原站。
题目描述
小 K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。 小 K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献 (如果他之前已经看过这篇参考文献的话就不用再看它了)。
假设洛谷博客里面一共有 n (1 ≤ n ≤ 10⁵) 篇文章(编号为 1 到 n)
以及 m (1 ≤ m ≤ 10⁶) 条参考文献引用关系。目前小 K 已经打开了编号为 1 的一篇文章,
请帮助小 K 设计一种方法,使小 K 可以不重复、不遗漏的看完所有他能看到的文章。
这边是已经整理好的参考文献关系图,其中,文献 X → Y 表示文章 X 有参考文献 Y。
不保证编号为 1 的文章没有被其他文章引用。

请对这个图分别进行 DFS 和 BFS,并输出遍历结果。 如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你可能需要先排序)。
输入格式
共 m + 1 行,第 1 行为 2 个数,n 和 m,分别表示一共有 n (1 ≤ n ≤ 10⁵) 篇文章
(编号为 1 到 n)以及 m (1 ≤ m ≤ 10⁶) 条参考文献引用关系。
接下来 m 行,每行有两个整数 X, Y 表示文章 X 有参考文献 Y。
输出格式
共 2 行。第一行为 DFS 遍历结果,第二行为 BFS 遍历结果。
输入输出样例
输入
8 9 1 2 1 3 1 4 2 5 2 6 3 7 4 7 4 8 7 8
输出
1 2 5 6 3 7 8 4 1 2 3 4 5 6 7 8
⚠ 注意这组样例的边恰好是按出发点升序、且每个点的出边也升序给的
(1→2,3,4;2→5,6;4→7,8……)。
这句话本身就是本页第 ① 步的全部内容:「忘了排序」这个错法,被这组样例完整地放过去了。
1⚠ 第一版:vector 存图,忘了 sort —— 而官方样例一点反应都没有
// ✗ P5318 第一版:vector 存图,**忘了排序** —— 而官方样例一点反应都没有。//// ⚠⚠ 这一版和上一道题 [B3643](/sol/b3643/) 的第一版是同一个 bug,可两组官方样例的态度正好相反:// B3643 那组样例当场把它打回来了(第三行邻接表就错);// 这道题的样例里,每个点的出边**恰好就是升序给的**(1→2,3,4;2→5,6;4→7,8……)// ⇒ 它原样打出正确答案。//// ⇒ ★★ **「样例挡不挡得住」不是这个 bug 的属性,只能一组一组试。**// (对拍 300 轮它被抓 97 次 —— 是「偶尔才错」型,这正是样例放过它的原因。)
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); // ★ 有向图:只存一遍 } /* ★ 少了这一行:for (int i = 1; i <= n; i++) sort(g[i].begin(), g[i].end()); */
string out; vector<char> vis(n + 1, 0); vector<int> st; st.push_back(1); while (!st.empty()) { int u = st.back(); st.pop_back(); if (vis[u]) continue; // ② 弹出时才判 vis[u] = 1; out += to_string(u); out += ' '; for (int i = (int)g[u].size() - 1; i >= 0; i--) // ① 逆序压栈 if (!vis[g[u][i]]) st.push_back(g[u][i]); } out += '\n';
fill(vis.begin(), vis.end(), 0); // ★★ 第二次遍历前必须清空 queue<int> q; q.push(1); vis[1] = 1; // ★ 入队就标记 while (!q.empty()) { int u = q.front(); q.pop(); out += to_string(u); out += ' '; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; q.push(v); } } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
上一道 B3643 的第一版是同一个错(vector 里邻居是输入序,不是升序)。
可那道题的官方样例当场把它打了回来(第三行邻接表打出 4 2 5 1 4),
这道题的样例一个字都没变。
| B3643 | P5318 | |
|---|---|---|
| 「忘了 sort」被官方样例挡住 | ★ 是 | ★ 否 |
| 300 轮对拍被抓 | 185(n 拧到 100 就是 300) |
97 |
⇒ ★★ 「样例挡不挡得住」不是这个 bug 的属性 —— 只能一组一组试。 (本书连着好几轮量到「官方样例是个『一测就死』的过滤器」: 它挡住的是「每组都错」的,放过的是「偶尔才错」的。这两页正好把两头都拿到了。)
2第二版:换成前向星 —— 同一句「顺序没管」,这次样例挡住了
// ✗ P5318 第二版:换成链式前向星,**邻居倒序出来**。//// 和第一版是同一句「顺序没管」,但方向相反:前向星把新边挂在链头 ⇒ 加边顺序的倒序。// ⚠ 这一版**官方样例挡得住**(DFS 第一步就从 1 走到 4 了),而第一版挡不住 ——// 同一组样例,对两个方向的「顺序错」态度不同。//// ★ 顺带:它也是「顺手写前向星」的默认后果 —— 前向星本身没有错,// 错的是**没有为「题目要求升序」这件事付钱**(见 [B3643](/sol/b3643/) 第 ⑤ 步:// 把倒序掰成升序,钱和直接 sort 一样多)。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<int> head(n + 1, -1), to_(m), nxt(m); int cnt = 0; for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; to_[cnt] = v; nxt[cnt] = head[u]; head[u] = cnt++; }
string out; vector<char> vis(n + 1, 0); vector<int> st; st.push_back(1); while (!st.empty()) { int u = st.back(); st.pop_back(); if (vis[u]) continue; vis[u] = 1; out += to_string(u); out += ' '; vector<int> nb; // 逆序压栈 —— 和正解同一套写法 for (int e = head[u]; e != -1; e = nxt[e]) nb.push_back(to_[e]); for (int i = (int)nb.size() - 1; i >= 0; i--) if (!vis[nb[i]]) st.push_back(nb[i]); } out += '\n';
fill(vis.begin(), vis.end(), 0); queue<int> q; q.push(1); vis[1] = 1; while (!q.empty()) { int u = q.front(); q.pop(); out += to_string(u); out += ' '; for (int e = head[u]; e != -1; e = nxt[e]) if (!vis[to_[e]]) { vis[to_[e]] = 1; q.push(to_[e]); } } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
打出来是 1 4 8 7 3 2 6 5 / 1 4 3 2 8 7 6 5 —— DFS 第一步就从 1 走到了 4。
3★★ 正解:sort + 迭代 DFS —— 而这道题连「用矩阵」这个选项都没有
// ★★ P5318 正解:vector 存图 + 每个点的出边 sort 一遍,**DFS 写成迭代的**,BFS 照常。//// 题面那半句「**如果有很多篇文章可以参阅,请先看编号较小的那篇**」就是全部难点 ——// 它要求**邻居按编号升序访问**,而三种存法给出的顺序各不相同(见 [B3643](/sol/b3643/) 第 ④ 步)。//// ⚠⚠ 而上一道题那条「白送的路」在这里**根本不存在**:// B3643 里 n ≤ 1000,邻接矩阵的一行天生就是升序,第二问白送;// 这道题 n ≤ 10⁵ ⇒ 矩阵要 10¹⁰ 格 = 9.31 GB,**连开都开不出来**。// ⇒ **存法是被 n 逼出来的,不是「哪个更漂亮」。**//// ⚠ DFS 为什么不写递归:顶格 n = 10⁵ 排成一条链时,// 「递归 + 边走边拼输出」那一版(p5318Rec.cpp)在本机 **2.4 万层**就爆栈了。// 解析页第 ⑦ 步量了这件事。//// ⚠ 迭代 DFS 要和递归版**输出一模一样**,有两处必须小心:// ① 邻居要**逆序**压栈(栈是后进先出,逆序压进去才会正序弹出来);// ② 标记要放在**弹出时**判一次(同一个点可能被压进去好几次)。//// 复杂度:建图 O(m)、排序 O(m log m)、两次遍历各 O(n + m)。顶格 n = 10⁵、m = 10⁶。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); // ★ 有向图:只存一遍 } for (int i = 1; i <= n; i++) sort(g[i].begin(), g[i].end());
string out; vector<char> vis(n + 1, 0); vector<int> st; st.push_back(1); while (!st.empty()) { int u = st.back(); st.pop_back(); if (vis[u]) continue; // ② 弹出时才判 vis[u] = 1; out += to_string(u); out += ' '; for (int i = (int)g[u].size() - 1; i >= 0; i--) // ① 逆序压栈 if (!vis[g[u][i]]) st.push_back(g[u][i]); } out += '\n';
fill(vis.begin(), vis.end(), 0); // ★★ 第二次遍历前必须清空 queue<int> q; q.push(1); vis[1] = 1; // ★ 入队就标记 while (!q.empty()) { int u = q.front(); q.pop(); out += to_string(u); out += ' '; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; q.push(v); } } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
B3643 里,「邻居按编号升序」是白送的 —— 因为 n ≤ 1000,
邻接矩阵的第 i 行天生就是升序,而那道题本来就要输出矩阵。
这道题 n ≤ 10⁵:
顶格 n = 10⁵、m = 10⁶ |
内存 |
|---|---|
| 邻接矩阵(就算一格只用 1 字节) | 10¹⁰ 格 = ★ 9.31 GB |
vector 邻接表 |
6.10 MB |
| 链式前向星 | 8.01 MB |
| (题面给的) | 128 MB |
⇒ ★★ 存法是被 n 逼出来的,不是「哪个更漂亮」。
同一个「按编号升序访问邻居」的要求,n = 1000 时可以靠矩阵白送,
n = 10⁵ 时只能老老实实排序。这就是第 29 章那句话的两面。
4第三个错法:两次遍历共用一个 vis,中间忘了清空
// ✗ P5318 第三版:DFS 和 BFS **共用一个 vis 数组,中间忘了清空**。//// 这是「一道题里要遍历两次」时最常见的错。它的表现极好认:// **第二行只剩一个 `1 `** —— 所有点在 DFS 那一轮已经被标记过了。//// ★ 它是「每一组都错」型(只要图里不止一个可达点就错),所以官方样例一测就死,// 对拍 300 轮也抓到 283 次。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m; if (!(cin >> n >> m)) return 0; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); // ★ 有向图:只存一遍 } for (int i = 1; i <= n; i++) sort(g[i].begin(), g[i].end());
string out; vector<char> vis(n + 1, 0); vector<int> st; st.push_back(1); while (!st.empty()) { int u = st.back(); st.pop_back(); if (vis[u]) continue; // ② 弹出时才判 vis[u] = 1; out += to_string(u); out += ' '; for (int i = (int)g[u].size() - 1; i >= 0; i--) // ① 逆序压栈 if (!vis[g[u][i]]) st.push_back(g[u][i]); } out += '\n';
/* ★ 少了这一行:fill(vis.begin(), vis.end(), 0); */ queue<int> q; q.push(1); while (!q.empty()) { int u = q.front(); q.pop(); out += to_string(u); out += ' '; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; q.push(v); } } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
它是「每一组都错」型(只要不止一个可达点就错)⇒ 官方样例一测就死,对拍 300 轮抓到 283 次。
5⚠ 第四个「错法」对拍永远抓不到 —— 而它在这道题上一分钱都不值
BFS 的「守卫」写在哪:入队就标记,还是出队才标记? 第 14 章 P1443 量过后者的代价是入队次数。这道题上呢:
本书前面量过两种「对拍 0 次」:概率低(换生成器就能抓)和结构性的 0(题面挡死 / 生成器缺一档)。 这里是第三种:两版的输出必然相同,跟数据一点关系都没有。
两行就能证:入队才标记的 BFS 里,v 第一次进队,是在它的某个前驱 u 被处理的那一刻;
出队才标记的 BFS 里,v 第一次进队还是在同一刻(只是后面可能再进几次)。
队列是先进先出 ⇒ 两边「第一次出队」的顺序一模一样 ⇒ 输出逐字节相同。
⇒ 所以只能数次数。而数出来的结果是个否定结论:
| 入队才标记 | 出队才标记 | 倍数 | |
|---|---|---|---|
随机小图(n = 10 / 100 / 1000,各 50 轮合计) |
229 / 998 / 12240 | 262 / 1079 / 13547 | 1.08 ~ 1.14 |
★ 顶格 n = 10⁵、m = 10⁶ |
99 993 | 500 315 | ★ 5.00 |
⇒ 顶格也只是五倍,而队列里放的是 int ⇒ 峰值 2 MB,题面给 128 MB。这道题上它挂不了人。
★ 对照 P1443 那次(24 × 24 的棋盘,每格能被塞进队列一万八千次):
「出队才标记要不要紧」的主语是「同一个点能被重复入队多少次」,
而这里它被 m ≤ 10⁶ 卡得死死的。
6★★★ 真正会挂人的是递归 —— 而「多少层会爆栈」不是一个能背的数
最自然的写法是递归 DFS,边走边把编号拼进输出串:
// ⚠ P5318 第一版之后最自然的写法:vector + sort + **递归 DFS**,边走边把编号拼进输出串。//// 算法完全正确(小数据上和正解逐字节相同),可它在**顶格数据上直接段错误** ——// 本机(`ulimit -s` = 8192 KB)实测:这个 dfs 每层要 **341 字节**,// 于是 **24 589 ~ 24 976 层**就把栈用光了,而题面 n 可以到 10⁵。//// ★★★ 解析页第 ⑦ 步把这件事量透了:**「多少层会爆栈」不是一个能背的数**,// 它是「栈上限 ÷ 每层字节数」,而每层字节数由**你在那个函数里写了什么**决定 ——// 只要把「拼输出」挪出递归(p5318Lean.cpp),门槛就从 2.4 万涨到 17.4 万,**差 7 倍**。//// 题面那半句「**如果有很多篇文章可以参阅,请先看编号较小的那篇**」就是全部难点 ——// 它要求**邻居按编号升序访问**,而三种存法给出的顺序各不相同(见 /sol/b3643/ 第 ④ 步)。//// ⚠⚠ 而上一道题那条「白送的路」在这里**根本不存在**:// B3643 里 n ≤ 1000,邻接矩阵的一行天生升序,第二问白送;// 这道题 n ≤ 10⁵ ⇒ 矩阵要 10¹⁰ 格,**连开都开不出来**。// ⇒ 存法的选择是被 n 逼出来的,不是「哪个更漂亮」。//// 复杂度:建图 O(m)、排序 O(m log m)、两次遍历各 O(n + m)。顶格 n = 10⁵、m = 10⁶。
#include <bits/stdc++.h>using namespace std;
int n, m;vector<vector<int>> g;vector<char> vis;string out;
void dfs(int u) { vis[u] = 1; out += to_string(u); out += ' '; for (int v : g[u]) if (!vis[v]) dfs(v);}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
if (!(cin >> n >> m)) return 0; g.assign(n + 1, {}); for (int i = 0; i < m; i++) { int u, v; cin >> u >> v; g[u].push_back(v); // ★ 有向图:只存一遍 } for (int i = 1; i <= n; i++) sort(g[i].begin(), g[i].end());
vis.assign(n + 1, 0); dfs(1); out += '\n';
vis.assign(n + 1, 0); // ★★ 第二次遍历前必须清空 queue<int> q; q.push(1); vis[1] = 1; // ★ 入队就标记 while (!q.empty()) { int u = q.front(); q.pop(); out += to_string(u); out += ' '; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; q.push(v); } } out += '\n'; fwrite(out.data(), 1, out.size(), stdout); return 0;}点「运行 ▶」看结果
它在样例上完全正确。可把顶格那条链(1 → 2 → … → 10⁵)喂进去,它一个字都不输出(段错误)。
本机(A 机 · WSL2 · i5-13500H · ulimit -s = 8192 KB · -O2 -std=c++17 · 2026-08-30)
二分出来的门槛:
| 写法 | 爆栈门槛(层) | ⇒ 每层约 | 顶格 n = 10⁵ |
|---|---|---|---|
递归 + 递归函数里 out += to_string(u) |
24 589 ~ 24 976 | 341 字节 | ★ 段错误 |
递归,但只往数组里塞一个 int(下面那版) |
174 000 ~ 174 500 | 48 字节 | 活着,⚠ 余量只有 1.74 倍 |
| 迭代(正解) | —— | —— | 稳 |
⇒ ★★★ 「多少层会爆栈」= 栈上限 ÷ 每层字节数,而每层字节数由你在那个函数里写了什么决定。
第 27 章那一轮量到的门槛是 14.2 万 ~ 15.6 万层(那个 dfs 的函数体是精简的),
和这一页的 2.4 万差 6 倍 —— 那不是两台机器的差别,是两个函数体的差别。
⚠ 顺带把那条规矩说得更狠一点:就算写成精简递归,顶格余量也只有 1.74 倍。 「顶格题一律写迭代,别赌」—— 赌赢了也只赢一点点。
7⚠ 和算法无关但会挂人的那一条:11 MB 的输入、1 秒的时限
m ≤ 10⁶ ⇒ 要读 2 × 10⁶ 个整数。顶格那组数据实测 11 778 709 字节(11.23 MB),而时限只有 1 秒。
同机、同一份建图 + 排序 + 两次遍历,只换读法(A 机 · WSL2 · 2026-08-30,独占):
| 读法 | 读入 + 建图 | 端到端 | 1 秒时限 |
|---|---|---|---|
默认 cin(不关同步) |
326 ms | 357 ms | 够,余量 2.8 倍 |
cin + sync_with_stdio(false) |
89 ms | 115 ms | 够,余量 8.7 倍 |
scanf |
120 ms | 147 ms | 够 |
getchar 快读 |
40 ms | 73 ms | 够 |
fread 快读 |
43 ms | 75 ms | 够 |
- 四种读法全都够 —— 又一次第 19 章 P1803 那条:
倍数跨题几乎不变,变的是绝对时间,而分数线画在绝对时间上。
⚠ 但默认
cin的余量只剩 2.8 倍,比 P1803 那道题(3 秒时限)紧得多 —— ⇒ 「要不要写快读」永远是「数的个数 × 每个数的耗时 ÷ 时限」这道算术题,不是习惯问题。 - ★ 关了同步的
cin又一次比scanf快(89 vs 120 ms)—— 这和第 6 章 P2367、第 12 章 P1226 是同一个方向,第三次了。 - ⚠ 这道题的输出只有两行、共
2n个数 —— 顶格约 1.2 MB, ★ 但别一个数一个cout <<地打:本页所有版本都是先拼成一个string再fwrite(B3643 第 ⑧ 步量过,这一步值 30 倍)。
8★ 三个错法各被抓多少(参照物就是正解自己的另一种写法)
300 轮(n 随机 3~10,边序打乱) |
|
|---|---|
| 忘了 sort | 97 |
| 前向星倒序 | 100 |
vis 忘清 |
283 |
| ⚠ 出队才标记 | ★ 0(可以证明的恒等,见第 ⑤ 步) |
★ 自检:把边按 (u, v) 排好再喂进去(这一档里邻接表天然升序),
「忘了 sort」当场变成精确的 0,而「前向星倒序」仍被抓 155 ——
⇒ 那个 0 是「这一档结构上抓不到」,不是这段代码没在跑。
9度量程序和生成器
10一页纸
| ★★ 关键的一步 | 题面那半句「先看编号较小的那篇」⇒ 每个点的出边要 sort |
| ★★ 和上一道的对照 | 同一个 bug:B3643 的样例挡住了,这道题的样例放过了(97/300 才错) |
| ★★★ 为什么不能用矩阵 | n = 10⁵ ⇒ 矩阵 9.31 GB,而邻接表只要 6.10 MB ⇒ 存法是被 n 逼出来的 |
| 第三个错法 | 两次遍历共用 vis 忘了清 ⇒ 第二行只剩 1 (283/300) |
| ⚠ 第四个不是错法 | BFS 出队才标记:输出必然逐字节相同(可证),代价顶格只有 5.00 倍入队 |
| ★★★ 真正会挂的 | 递归 DFS:每层 341 字节 ⇒ 2.4 万层就爆;把拼串挪出去 ⇒ 48 字节 / 17.4 万层(×7) |
| ⚠ 读入 | 11.23 MB / 1 秒:四种读法都够,但默认 cin 只剩 2.8 倍余量 |