第 24、26 节的 DFS 和 BFS 都在网格上跑:邻居不用存,四个方向数组现算一个出来就行。一旦题目直接给出 n 个点、m 条边,邻居就必须自己记下来。这一节只干两件事:边记在哪,记下来之后怎么走。
图有两种给法。隐式:点是格子,边由“能不能走”决定,邻居现算 —— 迷宫、马的遍历都属于这一类。显式:题目给一张边表,第 i 行 u v w 就是一条边。隐式图不需要存,显式图必须先存。本节讨论后者。
| 存法 | 空间 | 查 u→v | 枚举 u 的邻点 | 什么时候用 |
|---|---|---|---|---|
| 邻接矩阵 g[u][v] | O(n²) | O(1) | O(n) | n ≤ 3000、要频繁查边、Floyd(第 42 节) |
| 邻接表 vector | O(n + m),无向图 O(n + 2m) | O(deg(u)) | O(deg(u)) | 绝大多数题,m 到 10⁶ 也存得下 |
| 边集数组 (u,v,w) | O(m) | O(m) | O(m) | 只按边处理,如 Kruskal(第 43 节) |
算一次空间账就明白为什么必须用邻接表:n = 10⁵ 的邻接矩阵是 10¹⁰ 个 int,40 GB,而同样的图用邻接表只要 n 个 vector 对象(每个 24 字节,共 2.4 MB)加 2m 个 int(m = 10⁶ 时 8 MB)。反过来,n = 500 的小图用矩阵只有 1 MB,还能 O(1) 查边,这时邻接表反而自找麻烦。
存无向图时每条边要加两次。无向边 (u, v) 在内存里是两条有向弧:g[u].push_back(v) 和 g[v].push_back(u)。于是 m 条无向边写进表里是 2m 条记录,遍历所有邻点看到的总数也是 2m。三个后果:
2m 个位置。开 m 会越界,症状是 RE 或悄悄写坏旁边的数组。本节新增两个东西:一个能装下整张表的容器,一个能一次带走“终点 + 边权”的类型。后面所有图论题的建图部分,都从这两个里面选一个。
| 组成 | 含义 / 格式要求 |
|---|---|
vector<vector<int>> | 外层每个元素本身是一个 vector<int>。外层长度 = 点数,内层长度 = 该点的出度。整张表就是“每个点一条 list”。 |
g(n + 1) | 构造时给定外层大小。n + 1 是为了让下标 1..n 直接用;写 g(n) 再访问 g[n] 就越界。外层是运行期才知道的大小时,也可以先声明全局 vector<vector<int>> g;,再在 main 里 g.resize(n + 1);。 |
g[u].push_back(v) | 在 u 的邻点表末尾追加一个 v。有向图加一次;无向图 u、v 各加一次。 |
g[u].size() | u 的出度(无向图里是度数)。返回 size_t,与 int 混比会有符号警告,要倒序循环记得写 (int)g[u].size() - 1。 |
g[u][i] | 第 i 个邻点,i 从 0 到 size() − 1。越界不报错,只会读到随机值。 |
g[u].clear() | 只清掉 u 这一条表,外层长度不变。多测时清整张表用 g.assign(n + 1, vector<int>());。 |
for (int v : g[u]) | 范围 for,遍历 u 的全部邻点,比写下标短且不会写错边界。 |
int n = 4;
vector<vector<int>> g(n + 1); // 下标 0..4,0 号空着不用
g[1].push_back(2); // 有向边 1→2
g[1].push_back(3); // 有向边 1→3
g[3].push_back(1); // 无向边要两个方向各加一次
cout << g[1].size() << "\n"; // 2:点 1 有两条出边
for (int v : g[1]) cout << v << " "; // 2 3
vector<vector<int>> h; // 空的:一个点都没有,此时访问 h[1] 是越界
h.assign(n + 1, vector<int>()); // 变成 n+1 个空表,多测时用它清空两层尖括号在 C++11 之后可以直接连写 >>,不用写成 > >。
邻接表的内存是分段的:每个 g[i] 各自申请一块。所以不能对它 memcpy,也不能用 &g[u][0] 算指针偏移。
点数固定且不大时,竞赛里更常见的写法是 vector<int> g[N]; ——定长数组,每个元素是 vector,等价且不用 resize,但不能按输入动态开大小。
| 组成 | 含义 / 格式要求 |
|---|---|
struct Edge | 自定义类型:把“一条弧去哪、代价多少”捆成一个整体。用一个名字代替两个平行数组,以后要加“边编号”“反向边下标”也只改这一处。 |
int to | 这条弧的终点。无向图里同一对点会出现两次,方向相反。 |
int w | 边权。无权图可以整条删掉,只留 to —— 那就是 vector<int> 的邻接表。 |
vector<Edge> g[N] | 外层是定长数组,每个元素存“从 u 出发的所有弧”。和 vector<vector<Edge>> 完全等价,只是不用 resize。 |
push_back({v, w}) | 花括号按成员的声明顺序填,不是按名字。等价于 push_back(Edge{v, w})。 |
e.to / e.w | 成员变量,不是函数,不加括号。范围 for 用 for (auto &e : g[u]),写引用是为了不复制结构体。 |
无向图的加边 | 两次 push:g[u].push_back({v, w}); g[v].push_back({u, w});。权值相同,to 与起点互换。 |
struct Edge { int to, w; }; // to: 终点, w: 边权
vector<Edge> g[105]; // g[u] = 从 u 出发的所有弧
g[1].push_back({2, 7}); // 有向边 1→2,权 7
g[1].push_back({3, 2}); // 有向边 1→3,权 2
for (auto &e : g[1]) // 引用遍历,不复制
cout << e.to << " " << e.w << "\n"; // 2 7 / 3 2
cout << g[1][0].to << "\n"; // 2:像二维数组一样取第 0 条弧的终点{2, 7} 按声明顺序填。把 to、w 的声明顺序调换,{2, 7} 的含义就跟着变,而编译器一个字都不会提醒 —— 这类错只能靠自觉。
自己写了构造函数就不能再用花括号按位置初始化,除非保留一个全参数的构造函数。
无权图别硬套 Edge:多存一个用不上的 w,还多一次写错的机会。
有向还是无向?无向图每条边加两次,边集数组要开 2m。
要不要边权?不要就用 vector<int>,要就用 vector<Edge>。
下标从 1 还是从 0?从 1 开始就开 n + 1 个位置,别让 g[n] 越界。
洛谷博客里一共有 n 篇文章(编号 1 到 n)和 m 条参考文献引用关系。文献 X → Y 表示文章 X 有参考文献 Y。小 K 已经打开了编号为 1 的文章,他看一篇文章就一定会看它引用的全部文献(看过的不会再看)。
请对这张图分别做 DFS 和 BFS,输出遍历结果。如果有多个文献可以参阅,先看编号较小的那篇(因此你可能需要先排序)。
输入格式:第一行两个整数 n、m(1 ≤ n ≤ 10⁵,1 ≤ m ≤ 10⁶);接下来 m 行,每行两个整数 X、Y。不保证 1 号文章没有被其他文章引用。
输出格式:共两行。第一行为 DFS 遍历结果,第二行为 BFS 遍历结果,同一行内用空格分隔。
① 建图。有向图,m 条弧,g[X].push_back(Y) 一次就够。写成无向会凭空多出一半的边,可达的点变多,输出整片错。
② 排序。题目要“先看编号小的”。DFS 和 BFS 的访问顺序完全由邻接表里的存储顺序决定,不排序得到的是输入顺序决定的序列 —— 样例都过不了。排序总代价 Σ deg(u)·log deg(u) ≤ m log m。
③ 两个序列要分两次遍历,visited 必须重置。一次 DFS 跑完 vis 全是 true,接着跑 BFS 只会输出一个点。这是本题最高频的错误,且不排序、不清零这两件事在样例上都会暴露,所以样例一定要手推一遍。
④ 复杂度。建图 O(m),排序 O(m log m),两次遍历各 O(n + m)。m = 10⁶ 时总操作约 2×10⁷ 次,1 秒内。
// 骨架:建图 → 排序 → DFS → 清空 → BFS
for (int i = 0; i < m; ++i) { cin >> x >> y; g[x].push_back(y); } // 有向,只加一次
for (int u = 1; u <= n; ++u) sort(g[u].begin(), g[u].end()); // 让"最小的邻点"排在最前
dfs(1); cout << "\n"; // 先序输出整条 DFS 序列
memset(vis, 0, sizeof(vis)); // ★ 不清零,下一行只会输出一个 1
q.push(1); vis[1] = true; // BFS:入队即标记
while (!q.empty()) { /* 出队、输出、把未访问的邻点压进队 */ }DFS 用递归写最省事:递归调用栈本身就是“走到黑再退回”的那条路径。BFS 用 queue,第 26 节已经写熟了,这里只换掉“邻居怎么来”——从方向数组换成邻接表。另外记住一条:DFS 可以递归也可以显式栈,BFS 只能队列,因为它们对“下一个访问谁”的要求不同。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m;
vector<vector<int>> g; // g[u] = u 引用的所有文献
bool vis[N]; // 访问标记:两个序列共用,用前都要清
void dfs(int u) {
vis[u] = true;
cout << u << " "; // 先序:一进点就输出,这就是"DFS 序列"
for (int v : g[u]) // g[u] 已升序,天然满足"先看编号小的"
if (!vis[v]) dfs(v); // 有环时必须判重,否则无限递归
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
g.resize(n + 1); // n+1 条空表,下标 1..n 可用
for (int i = 0; i < m; ++i) {
int x, y;
cin >> x >> y;
g[x].push_back(y); // 有向边 X→Y,只加一个方向
}
for (int u = 1; u <= n; ++u) // 每个邻接表单独升序
sort(g[u].begin(), g[u].end());
dfs(1); // 从 1 号文章出发
cout << "\n";
memset(vis, 0, sizeof(vis)); // ★ 第二轮必须清空
queue<int> q;
q.push(1);
vis[1] = true; // 入队即标记,不能等出队
while (!q.empty()) {
int u = q.front();
q.pop(); // front 取值、pop 删除,两句不能合并
cout << u << " ";
for (int v : g[u])
if (!vis[v]) { // 判重同时保证"每个点只进队一次"
vis[v] = true;
q.push(v);
}
}
cout << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
const int N = 100005; | n 到 10⁵,开 100005 留 5 个余量。正好开 100000 时,u = 100000 会踩到 vis[100000] 之外的一格。 |
vector<vector<int>> g; | 定义成全局是为了 dfs 能直接看见它,省一个“传引用”的参数。外层大小要到读入 n 之后才知道,所以先声明、再 resize。 |
g.resize(n + 1); | 变成 n + 1 条空表。多测时这一句同时就是“清空整张图” —— resize 到相同大小不会清内容,所以多测要么先 g.clear() 再 resize,要么直接 assign。 |
g[x].push_back(y); | 一条有向弧。P5318 是“X 引用 Y”,单向;加成双向会把可达点放大,输出多出一堆不该出现的编号。 |
sort(g[u].begin(), g[u].end()); | 题目要“先看编号小的”。排的是每个点自己的邻点表,不是把 1..n 排序。总代价 O(m log m),m = 10⁶ 时约 2×10⁷ 次比较。 |
vis[u] = true; cout << u << " "; | 先序输出:进点即打印。DFS 的“先/中/后”三种序只差这一句的位置,图没有中序,绝大多数题要的是先序。 |
for (int v : g[u]) if (!vis[v]) dfs(v); | 范围 for 直接遍历邻点,已访问的必须跳过。忘了判重,有环时立刻无限递归,表现为 RE(栈溢出)。 |
dfs(1); | 源点是 1 号文章。题目只要求“他能看到的”,别的点即使能走到 1 也不输出。 |
memset(vis, 0, sizeof(vis)); | ★ 整节最关键的一行。不清零,vis 全为 true,BFS 只输出 1 就结束,第二个序列直接废掉。 |
q.push(1); vis[1] = true; | BFS 入队即标记。等出队才标记的话,同一个点会被多个前驱各 push 一次,序列里出现重复编号。 |
int u = q.front(); q.pop(); | 先取值再删除。int u = q.pop() 编译不过:pop() 返回 void。 |
cout << "\n"; | 两个序列各占一行,行末不留空格。DFS 的循环里输出的是“编号 + 空格”,所以最后一个空格之后才换行。 |
无向图里,能互相到达的点构成一个连通块。数连通块的做法只有一行新代码:外层扫一遍所有点,遇到没访问过的,就说明发现了一个新块,从这里跑一次 DFS 把整块点亮。
int blocks = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) { // 这个点没被任何已发现的块扫到 → 新块
++blocks;
dfs(i); // 一次 DFS 把整个块标记完
}
cout << blocks << "\n";为什么总复杂度还是 O(n + m)。每次 dfs(i) 只在 vis[i] 为假时启动,而它会把进入的每个点立即标记,所以每个点恰好被进入一次;每个点的邻点表在它自己那次进入时被遍历一次,无向边被两端各看一次,合计 2m 次。外层循环不是乘上去的,因为大多数点会被 continue 掉。
这个外层循环是图论里最常用的一段代码。它其实在回答三个问题:图连不连通(blocks == 1)、有几个独立的部分(blocks 的值)、每个点属于哪个部分(把 ++blocks 换成 block[i] = blocks 在 dfs 里赋值)。有向图里对应的概念叫强连通分量,本节不展开。
再顺手记一个数:若图有 n 个点、m 条边、k 个连通块,则 k ≥ n − m。因为每个连通块至少要有一棵生成树,树边共 n − k 条,而 m 不小于树边数。这个不等式能用来粗查“边数是不是少得可疑”。
二分图:能把点集分成两类,使每条边的两个端点分属不同类。等价说法是图中没有奇环(长度为奇数的环)。判定只要给每个点染一种颜色,要求一条边的两端颜色不同。
染色规则只有一句:color[v] = 3 - color[u](颜色取 1 和 2)。若某个邻点已经染过、且颜色与 u 相同,就出现了同色边,说明有奇环,直接返回 false。
int color[N]; // 0 = 未染色, 1 / 2 = 两种颜色
bool isBipartite() {
for (int s = 1; s <= n; ++s) { // ★ 每个连通块都要单独起一次
if (color[s]) continue;
color[s] = 1; // 块内第一个点随便染
queue<int> q; q.push(s);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u]) {
if (!color[v]) { // 没染过:强制染成反色
color[v] = 3 - color[u];
q.push(v);
} else if (color[v] == color[u]) {
return false; // 同色相邻 → 奇环 → 不是二分图
}
}
}
}
return true;
}为什么这样一定对。一条边两端必须异色,所以一个连通块里只要定下任意一个点的颜色,其余所有点的颜色都被唯一确定(沿路径一路翻色)。两种整体配色只差“把 1 和 2 对调”,对判定没有影响。因此“染色过程中出现同色边”当且仅当存在奇环。
三个必须注意的地方:①必须对每个连通块各起一次 BFS —— 只从 1 号点染,别的块全是 0,永远判成二分图;②这只适用于无向图,有向图的“二分”是另一个概念;③自环 (u, u) 会让 color[v] == color[u] 当场成立,立刻判 false,这正好符合“自环是长度 1 的奇环”。复杂度同样是 O(n + m)。
递归 DFS 的深度等于最长的一条链。本题 n 能到 10⁵,如果数据是一条 10⁵ 长的链,递归深度就是 10⁵,每层几十字节,合计几 MB —— 在栈限制 8 MB 的评测机上会 RE。洛谷的栈空间通常够用,但你必须知道这个风险:数组没越界却 RE,先怀疑递归深度。
解法是把“调用栈”自己用一个 vector 实现。顺序能完全一致,关键在逆序压栈:把 u 的邻点从大到小压进去,编号最小的那个就落在栈顶,正好等于递归时“先走最小的那条边”。
void dfs_iter(int s) {
vector<int> st; // 自己当栈用:back() 看栈顶,push_back 压栈
st.push_back(s);
while (!st.empty()) {
int u = st.back();
st.pop_back();
if (vis[u]) continue; // 同一个点可能被多个前驱压进来
vis[u] = true;
cout << u << " "; // 出栈即输出,位置等价于递归里的"刚进函数"
// 逆序压栈:让编号最小的邻点留在栈顶
for (int i = (int)g[u].size() - 1; i >= 0; --i)
if (!vis[g[u][i]]) st.push_back(g[u][i]);
}
}代价是栈里最多压 m 条记录(一个点可能被多个前驱重复压栈),m = 10⁶ 时 4 MB,可以接受。顺便记住这个对应关系:递归 = 系统栈,显式栈 = 自己管;DFS 用栈,BFS 用队列。把 queue 换成 stack,BFS 就变成了“深度优先但顺序不对”的东西,这也说明两种容器的区别不只是写法。
无向图只加了一次边(漏了 g[v].push_back(u)):从 v 出发回到 u 的路消失,连通块数偏大、可达点偏少 → WA。样例常是树,容易蒙过去。
两个序列共用 vis 却不重置:第二个序列只剩源点 → WA,且错得非常整齐(一行只有一个数)。
BFS 在出队时才标记:同一个点被多个前驱各压一次,序列里出现重复编号 → WA。
忘记 sort 邻接表:输出的是输入顺序决定的序列,与“先看编号小的”不符 → WA。这种错样例就能查出来,属于最好查的一类,务必先手推样例。
数组开 vector<int> g[100000] 而 n 正好是 10⁵:下标 g[100000] 越界 → RE 或随机 WA。习惯写 N + 5。
有环图删掉 if (!vis[v]):递归无出口,几秒内爆栈 → RE(报错信息通常是 Stack overflow / Segmentation fault)。
链状数据上递归 DFS,深度 10⁵ → RE,而且数组完全没越界。改显式栈即可。
邻接矩阵开在函数里:n = 5000 时 5000² × 4 字节 = 100 MB,直接爆栈 → RE。大数组一律放全局或静态区(全局数组在 BSS 段,不受 8 MB 栈限制)。
关掉页面,从空文件把 P5318 敲一遍并提交。
用样例里的 8 个点、9 条边手推两个序列,再和程序输出逐位比对:DFS 应为 1 2 4 8 5 3 6 7,BFS 应为 1 2 3 4 5 6 7 8。
改一改①:在 g[x].push_back(y) 后面补一句 g[y].push_back(x)(当成无向图),同一组数据重跑 —— 两个序列居然一模一样。因为新增的反向边全部指向已经访问过的点。这就是“样例查不出方向错误”的原因:改一条边为 7 1(或任何指向未访问点的边)再看,顺序立刻变。
改一改②:删掉 sort 那一行。样例的两个序列都变,说明顺序由邻接表的存储顺序决定。
改一改③:删掉 memset(vis, 0, sizeof(vis))。BFS 那一行只剩一个 1 ——这是本节最高频的错,亲眼见一次比读十遍有用。
改一改④:把 vis 换成 int block[N],先补成无向图,再按连通块给每个点标号,输出块数和每个点所属的块号,看看 1 号文章到底能不能到全部 8 篇。
无向图里,点 u 的度 deg(u) 是与它相连的边数。有向图分成出度(从 u 出发的边数)和入度。邻接表里 g[u].size() 在有向图中就是出度,这一点在读题时经常要用。
握手引理:Σu deg(u) = 2m。理由只有一句:每条边 u–v 给 deg(u) 贡献 1、给 deg(v) 贡献 1,被数了两次。有向图里同样的道理给出 Σ out(u) = Σ in(u) = m。
推论(很实用):度数为奇数的点一定有偶数个。这就是欧拉路径判定里“奇点只有 0 个或 2 个”的来源,也是很多构造题的突破口 —— 如果一个构造要求“每个点的度数都是奇数”,那么点数必须是偶数,否则无解。
空间账。设 int 占 4 字节,一个空的 vector 对象占 24 字节:
| n | m | 邻接矩阵 n² × 4 | 邻接表(无向)n × 24 + 2m × 4 |
|---|---|---|---|
| 10³ | 10⁵ | 4 MB | 24 KB + 0.8 MB ≈ 0.8 MB |
| 10⁴ | 10⁵ | 400 MB(存不下) | 0.24 MB + 0.8 MB ≈ 1 MB |
| 10⁵ | 10⁶ | 40 GB(存不下) | 2.4 MB + 8 MB ≈ 10 MB |
表里能读出一条分界线:n ≤ 3000 时矩阵还能用(36 MB,放全局),n ≥ 10⁴ 时矩阵必死(400 MB 起)。所以“n ≤ 500 的 Floyd 题”用矩阵,“n 到 10⁵ 的遍历题”用邻接表,不是风格问题,是能不能存下的问题。
遍历的代价。DFS 每个点进一次、每条邻点表扫一次;无向图里邻点表的总长度是 2m,所以是 O(n + 2m) = O(n + m)。递归深度不超过 n,BFS 队列长度也不超过 n。本题 n = 10⁵、m = 10⁶,两次遍历共约 2×(10⁵ + 2×10⁶) ≈ 4×10⁶ 次操作,加上排序的 2×10⁷ 次比较 —— 瓶颈在排序,不在遍历,这是很多图论题的真实情况。
最后记一个二分图的计数结论:n 个点的二分图最多有 ⌊n²/4⌋ 条边(Mantel 定理),取到等号的是两部各 n/2 个点的完全二分图。所以看到“二分图 + 边数接近 n²/4”就该警觉:这图是稠密的,邻接矩阵可能比邻接表更合适。
vector<vector<int>> 换成邻接矩阵,哪些题会 TLE 或 MLE?换成边集数组呢?分别写出 n = 10⁵、m = 10⁶ 时的估算。给出 N 个点、M 条边的有向图。对每个点 v,令 A(v) 表示从点 v 出发能到达的编号最大的点。请求出 A(1), A(2), …, A(N)。
输入格式:第一行两个整数 N、M(1 ≤ N, M ≤ 10⁵);接下来 M 行,每行两个整数 U、V,表示一条边 U → V。点用 1..N 编号。
输出格式:一行 N 个整数,依次为 A(1) 到 A(N),用空格分隔。
对每个点各跑一次 DFS 是 O(N(N + M)),N = 10⁵ 时是 10¹⁰ 量级,必然超时。换个方向想。
“从 v 出发能到 w”和“在反图里从 w 出发能到 v”是同一句话。反图就是把每条边掉个头。
先处理编号大的点:在反图上从 w 出发,一次能扫到的所有点,它们的答案至少是 w。从 N 递减到 1 走一遍,每个点只需要被扫到一次。
建反图:原边 u → v 在反图里存成 v → u,即 rg[v].push_back(u)。这样“反图上从 w 出发能到达 x”就等价于“原图上 x 能到达 w”。
ans[] 全部先置 0,vis[] 全部先置 false。
for w = N down to 1:若 vis[w] 为假,就把 ans[w] 记成 w,然后从 w 出发在反图上做一次 BFS/DFS,把遇到的每个未访问点 x 都标成 vis[x] = true; ans[x] = w;。
正确性:处理到 w 时,所有编号大于 w 的点都已经处理完,它们能到达的点全被标记过。若 x 此刻还没被标记,说明原图上 x 到不了任何编号大于 w 的点;而反图上从 w 能扫到 x,说明原图上 x 能到 w。两者合起来,x 能到达的最大编号正好是 w。
若 w 已标记就整块跳过:此时原图上 w 能到某个编号更大的点,w 的答案已经定了;能到达 w 的点当然也能到达那个更大的点,答案同样已经定了,不必再扫。
复杂度:每个点在反图上最多被扫到一次,每条反边最多被看一次,O(N + M),N = M = 10⁵ 时总操作不到 2×10⁵ 次。
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m;
int ans[N]; // ans[v]:从 v 出发能到达的最大编号
bool vis[N];
vector<vector<int>> rg; // 反图:rg[v] 存"能一步到达 v 的点"
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
rg.resize(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
rg[v].push_back(u); // 原边 u→v,反图里是 v→u
}
queue<int> q;
for (int w = n; w >= 1; --w) { // 从大到小,先定大答案
if (vis[w]) continue; // 已被更大的点扫过,答案已定
vis[w] = true;
ans[w] = w; // w 能到自己,且到不了更大的点
q.push(w);
while (!q.empty()) { // 反图上从 w 扩散
int u = q.front();
q.pop();
for (int x : rg[u])
if (!vis[x]) { // 每个点只标一次 → O(N+M)
vis[x] = true;
ans[x] = w;
q.push(x);
}
}
}
for (int i = 1; i <= n; ++i)
cout << ans[i] << (i == n ? '\n' : ' ');
return 0;
}| 片段 | 作用 |
|---|---|
rg[v].push_back(u); | 反着存。这是全题的关键一步:把“从 v 出发能到谁”变成“谁一步能到 v”,于是从一个大声号的点出发,就能一次扫出“能到达它”的所有点。 |
for (int w = n; w >= 1; --w) | 从大到小枚举。只有“先处理大的”,第一次被标记才一定是最大值。写成从小到大答案全错,而且错得很有规律:每个点都得到最小的那个可达点。 |
if (vis[w]) continue; | w 已被更大的点扫过,说明原图上 w 能到达编号更大的点,w 的答案已经定了;能到达 w 的点答案也早就定了,整块不用再扫。这一步保证总复杂度是 O(N + M)。 |
ans[w] = w; | w 至少能到自己,所以 A(w) ≥ w;而所有比 w 大的点都没扫到过 w,说明 w 到不了它们,所以 A(w) 正好等于 w。 |
q.push(w); | 用队列做 BFS。这里用 DFS 也对(顺序不影响标记结果),但 BFS 不会递归,天然避开 10⁵ 长链的爆栈问题。 |
for (int x : rg[u]) if (!vis[x]) | 把“能一步到 u”的点全摊开,它们都能沿原图 u 继续走到 w。判重让每个点只被标记一次。 |
ans[x] = w; | 给新点打上答案。注意写的是 w(这一轮出发的大点),不是 u、也不是 x。 |
(i == n ? '\n' : ' ') | 行末只换行、不多空格。N 个数一行时这个细节直接决定 WA 与否。 |
100000(而 N 能到 10⁵):ans[100000]、vis[100000] 越界,→ RE 或随机 WA,习惯写 N + 5。