题目描述(中文翻译)
给定一张航线图,请你判断任意一对城市之间是否可以规划往返行程(round trip)。
输入说明
每个输入文件包含一个测试用例。第一行给出两个正整数 N(2 ≤ N ≤ 10^4)和 M(M ≤ 6N),分别是城市总数(城市编号 1 ~ N)和航线数。随后 M 行,每行先给出航线出发城市编号,再给出目的城市编号,以空格分隔。题目保证出发地与目的地不同。
航线信息之后给出正整数 K,即查询数。随后 K 行查询,每行包含两个不同城市的编号。
输出说明
对每个查询,若可以往返,输出 Yes,否则输出 No。
样例输入
in
12 19
3 4
1 3
12 11
5 9
6 2
3 2
10 7
9 1
7 12
2 4
9 5
2 6
12 4
11 10
4 8
8 12
11 8
12 7
1 5
20
11 4
12 7
3 6
2 3
5 3
3 9
4 3
8 3
8 10
10 11
7 8
7 1
9 5
1 9
2 6
样例输出
out
Yes
Yes
No
No
No
No
No
No
Yes
Yes
Yes
No
Yes
Yes
Yes
No
No
No
No
No
数据说明 :附件样例中
K = 20,但实际只列出了 15 条查询(样例输出有 20 行)。本文的算法分析与代码对任意K均成立;手工 SCC 分析与样例前 15 条输出完全吻合。
一、解题思路
1. 把航线建成"有向图"
每条航线 u v 表示航班只能从 u 飞到 v,方向不可忽略:
- 城市 = 顶点(共 N 个,编号 1 ~ N);
- 航线 = 有向边 u → v(共 M 条)。
2. 理解"往返"的数学含义
两个城市 a、b 之间能规划往返,要求同时满足:
a 能到达 b (a 沿有向边一路飞到 b)
b 能到达 a (b 沿有向边一路飞回 a)
这正是强连通(Strongly Connected)的定义。有向图中互相可达的顶点构成的极大子图,称为强连通分量(Strongly Connected Component,SCC)。因此:
结论:a、b 能往返,当且仅当它们属于同一个强连通分量。
3. 先离线求 SCC,再 O(1) 回答查询
如果对每个查询临时做两遍 DFS 判断可达性,总代价是 K × O(N+M),查询一多必然超时。正确做法是:
- 读入全部边后,一次性 求出图中所有 SCC,给每个城市打上分量编号
comp[u]; - 每个查询只需比较编号:
comp[a] == comp[b]输出Yes,否则No,单次查询O(1)。
4. 样例图的 SCC 手工划分
根据 19 条有向边,可把 12 个城市划分为 4 个强连通分量:
| 分量编号 | 城市集合 | 互相可达的依据 |
|---|---|---|
| ① | {1, 5, 9} |
1→5→9→1 构成环 |
| ② | {3} |
只有 1→3 指向它,3 无法回到任何前驱 |
| ③ | {2, 6} |
2→6 且 6→2 |
| ④ | {4, 7, 8, 10, 11, 12} |
4→8→12→4 成环,12↔7、12→11→10→7 全部互通 |
对照前 15 条查询:
11 4同属分量④ → Yes;12 7同属④ → Yes;3 6分属②、③ → No;9 5同属① → Yes;2 6同属③ → Yes。
与样例输出逐条一致。
5. 两种主流 SCC 算法
| 算法 | 核心手段 | DFS 次数 | 特点 |
|---|---|---|---|
| Kosaraju | 原图 DFS 记完成顺序,再在逆图上按序 DFS | 2 次 | 原理直观、代码不易写错,推荐入门使用 |
| Tarjan | 用 dfn/low 时间戳在一次 DFS 中识别 SCC | 1 次 | 工业界常用,需要理解回溯与回边 |
两者时间复杂度都是线性的 O(N+M)。
二、解题流程图
下图描述从建模到回答查询的整体思维过程:
#mermaid-svg-2tGAuuc549mKSTiK{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2tGAuuc549mKSTiK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2tGAuuc549mKSTiK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2tGAuuc549mKSTiK .error-icon{fill:#552222;}#mermaid-svg-2tGAuuc549mKSTiK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2tGAuuc549mKSTiK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2tGAuuc549mKSTiK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2tGAuuc549mKSTiK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2tGAuuc549mKSTiK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2tGAuuc549mKSTiK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2tGAuuc549mKSTiK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2tGAuuc549mKSTiK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2tGAuuc549mKSTiK .marker.cross{stroke:#333333;}#mermaid-svg-2tGAuuc549mKSTiK svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2tGAuuc549mKSTiK p{margin:0;}#mermaid-svg-2tGAuuc549mKSTiK .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2tGAuuc549mKSTiK .cluster-label text{fill:#333;}#mermaid-svg-2tGAuuc549mKSTiK .cluster-label span{color:#333;}#mermaid-svg-2tGAuuc549mKSTiK .cluster-label span p{background-color:transparent;}#mermaid-svg-2tGAuuc549mKSTiK .label text,#mermaid-svg-2tGAuuc549mKSTiK span{fill:#333;color:#333;}#mermaid-svg-2tGAuuc549mKSTiK .node rect,#mermaid-svg-2tGAuuc549mKSTiK .node circle,#mermaid-svg-2tGAuuc549mKSTiK .node ellipse,#mermaid-svg-2tGAuuc549mKSTiK .node polygon,#mermaid-svg-2tGAuuc549mKSTiK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2tGAuuc549mKSTiK .rough-node .label text,#mermaid-svg-2tGAuuc549mKSTiK .node .label text,#mermaid-svg-2tGAuuc549mKSTiK .image-shape .label,#mermaid-svg-2tGAuuc549mKSTiK .icon-shape .label{text-anchor:middle;}#mermaid-svg-2tGAuuc549mKSTiK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2tGAuuc549mKSTiK .rough-node .label,#mermaid-svg-2tGAuuc549mKSTiK .node .label,#mermaid-svg-2tGAuuc549mKSTiK .image-shape .label,#mermaid-svg-2tGAuuc549mKSTiK .icon-shape .label{text-align:center;}#mermaid-svg-2tGAuuc549mKSTiK .node.clickable{cursor:pointer;}#mermaid-svg-2tGAuuc549mKSTiK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2tGAuuc549mKSTiK .arrowheadPath{fill:#333333;}#mermaid-svg-2tGAuuc549mKSTiK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2tGAuuc549mKSTiK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2tGAuuc549mKSTiK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2tGAuuc549mKSTiK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2tGAuuc549mKSTiK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2tGAuuc549mKSTiK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2tGAuuc549mKSTiK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2tGAuuc549mKSTiK .cluster text{fill:#333;}#mermaid-svg-2tGAuuc549mKSTiK .cluster span{color:#333;}#mermaid-svg-2tGAuuc549mKSTiK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2tGAuuc549mKSTiK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2tGAuuc549mKSTiK rect.text{fill:none;stroke-width:0;}#mermaid-svg-2tGAuuc549mKSTiK .icon-shape,#mermaid-svg-2tGAuuc549mKSTiK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2tGAuuc549mKSTiK .icon-shape p,#mermaid-svg-2tGAuuc549mKSTiK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2tGAuuc549mKSTiK .icon-shape .label rect,#mermaid-svg-2tGAuuc549mKSTiK .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2tGAuuc549mKSTiK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2tGAuuc549mKSTiK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2tGAuuc549mKSTiK :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
开始:读入 N、M 与 M 条有向航线
建立有向图邻接表(含逆图)
求所有强连通分量 SCC
给每个城市标记分量编号 comp
读入一个查询 a、b
compa = compb ?
输出 Yes
输出 No
还有查询?
结束
三、代码流程说明
以推荐的 Kosaraju 算法 + 数组邻接表为例,程序分为 6 个阶段:
- 建表 :用
head / to / next三个数组模拟邻接表。head[u]存城市 u 的第一条边编号,to[e]存边 e 的终点,next[e]存同一起点的下一条边,边编号 0 表示空;同时维护一套逆图(每条边方向反转)。 - 读边:每读入一条 u → v,在原图加 u → v、在逆图加 v → u。
- 第一遍 DFS(原图) :对每个未访问城市启动 DFS,在递归返回时 (后序)把城市压入
order,得到按完成时间排列的序列。 - 第二遍 DFS(逆图) :按
order从后往前取城市,若尚未分配分量则在逆图上 DFS,本次 DFS 到达的所有城市属于同一个 SCC,统一写入分量编号。 - 处理查询 :读入 K,逐条读入城市对,比较分量编号,用三元运算符选择输出
Yes或No。 - 结束 :
return 0正常退出。
Kosaraju 为什么需要"逆图 + 完成顺序"
把每个 SCC 缩成一个点后,整张图变成一个有向无环图(DAG)。第一遍 DFS 的完成顺序保证:DAG 中越靠近"源"的分量越晚被处理;在逆图上从该分量出发 DFS 时,逆图只保留分量内部的双向可达性,无法跨到其他分量,因此每次 DFS 恰好干净地切出一个 SCC。
四、代码流程图
下图对应 Kosaraju 版本的实际执行路径:
#mermaid-svg-6riWdIojKMEnj4FG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6riWdIojKMEnj4FG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6riWdIojKMEnj4FG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6riWdIojKMEnj4FG .error-icon{fill:#552222;}#mermaid-svg-6riWdIojKMEnj4FG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6riWdIojKMEnj4FG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6riWdIojKMEnj4FG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6riWdIojKMEnj4FG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6riWdIojKMEnj4FG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6riWdIojKMEnj4FG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6riWdIojKMEnj4FG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6riWdIojKMEnj4FG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6riWdIojKMEnj4FG .marker.cross{stroke:#333333;}#mermaid-svg-6riWdIojKMEnj4FG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6riWdIojKMEnj4FG p{margin:0;}#mermaid-svg-6riWdIojKMEnj4FG .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6riWdIojKMEnj4FG .cluster-label text{fill:#333;}#mermaid-svg-6riWdIojKMEnj4FG .cluster-label span{color:#333;}#mermaid-svg-6riWdIojKMEnj4FG .cluster-label span p{background-color:transparent;}#mermaid-svg-6riWdIojKMEnj4FG .label text,#mermaid-svg-6riWdIojKMEnj4FG span{fill:#333;color:#333;}#mermaid-svg-6riWdIojKMEnj4FG .node rect,#mermaid-svg-6riWdIojKMEnj4FG .node circle,#mermaid-svg-6riWdIojKMEnj4FG .node ellipse,#mermaid-svg-6riWdIojKMEnj4FG .node polygon,#mermaid-svg-6riWdIojKMEnj4FG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6riWdIojKMEnj4FG .rough-node .label text,#mermaid-svg-6riWdIojKMEnj4FG .node .label text,#mermaid-svg-6riWdIojKMEnj4FG .image-shape .label,#mermaid-svg-6riWdIojKMEnj4FG .icon-shape .label{text-anchor:middle;}#mermaid-svg-6riWdIojKMEnj4FG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6riWdIojKMEnj4FG .rough-node .label,#mermaid-svg-6riWdIojKMEnj4FG .node .label,#mermaid-svg-6riWdIojKMEnj4FG .image-shape .label,#mermaid-svg-6riWdIojKMEnj4FG .icon-shape .label{text-align:center;}#mermaid-svg-6riWdIojKMEnj4FG .node.clickable{cursor:pointer;}#mermaid-svg-6riWdIojKMEnj4FG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6riWdIojKMEnj4FG .arrowheadPath{fill:#333333;}#mermaid-svg-6riWdIojKMEnj4FG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6riWdIojKMEnj4FG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6riWdIojKMEnj4FG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6riWdIojKMEnj4FG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6riWdIojKMEnj4FG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6riWdIojKMEnj4FG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6riWdIojKMEnj4FG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6riWdIojKMEnj4FG .cluster text{fill:#333;}#mermaid-svg-6riWdIojKMEnj4FG .cluster span{color:#333;}#mermaid-svg-6riWdIojKMEnj4FG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6riWdIojKMEnj4FG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6riWdIojKMEnj4FG rect.text{fill:none;stroke-width:0;}#mermaid-svg-6riWdIojKMEnj4FG .icon-shape,#mermaid-svg-6riWdIojKMEnj4FG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6riWdIojKMEnj4FG .icon-shape p,#mermaid-svg-6riWdIojKMEnj4FG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6riWdIojKMEnj4FG .icon-shape .label rect,#mermaid-svg-6riWdIojKMEnj4FG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6riWdIojKMEnj4FG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6riWdIojKMEnj4FG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6riWdIojKMEnj4FG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
程序开始
初始化正图、逆图邻接表
循环读入 M 条边,正图逆图同时加边
第一遍 DFS:正图遍历,后序记录 order
按 order 逆序取城市
该城市已分配分量?
逆图 DFS,标记同一个分量编号
取下一个城市
序列处理完?
读 K 个查询,比较 comp 输出 Yes/No
return 0
五、算法选型与复杂度
方案对比
| 方案 | 预处理 | 单次查询 | 总时间复杂度 | 评价 |
|---|---|---|---|---|
| 每查询双 DFS 判可达 | 无 | O(N+M) | O(K(N+M)) | 400 ms 下 K 稍大即超时,不可取 |
| Kosaraju 求 SCC | O(N+M) | O(1) | O(N+M+K) | 推荐,两次 DFS 结构清晰 |
| Tarjan 求 SCC | O(N+M) | O(1) | O(N+M+K) | 一次 DFS,代码略紧凑 |
| 并查集 | O(M α(N)) | O(α(N)) | O(M α(N)+K) | 仅适用无向图,本题有向不能用 |
| Floyd 传递闭包 | O(N^3) | O(1) | O(N^3) | N = 10^4 完全不可行 |
复杂度与空间核算
- 时间复杂度 :
O(N + M + K),建图与两遍 DFS 各线性扫描一次,K 个查询常数时间。 - 空间复杂度 :
O(N + M),邻接表(正图 + 逆图)与若干标记数组。按最大规模 N = 10^4、M = 6×10^4 估算,边数组合计约 1 MB 左右,远低于 64 MB 限制。
六、易错点与边界情况
易错点
- 把有向图当成无向图。航线有方向,只有 a→b 而没有回程路径时不能往返;这是本题最根本的概念点。
- 误用并查集 。并查集只能表达"无向连通",无法表达有向可达,直接
union(u, v)会把大量单向可达的城市错误判为 Yes。 - 每个查询临时 DFS/BFS。总复杂度随 K 膨胀,是本题主要的超时原因;必须先离线求 SCC。
- 第一遍 DFS 顺序记录错误 。城市必须在递归返回之后 (后序)写入
order,写成访问时立即记录(前序)会导致第二遍切分错误。 - 第二遍忘记换逆图或顺序搞反 。必须在逆图上、按完成序列从后往前处理,三个条件缺一不可。
- 忘记图可能不连通 。两遍 DFS 都要套一层
for (i = 1; i <= N; i++)循环,从每个未访问城市重新启动。 - 边数组开小导致越界。M 最大为 6N = 60000,正图、逆图各需容纳 M 条边,容量按 60005 开。
- 递归深度风险。图为一条长链时 DFS 递归深度可达 10^4 层。本题规模下默认栈通常可承受;若环境栈很小,需改写为显式栈的迭代 DFS。
- 输出大小写不符 。必须严格输出
Yes/No,写成YES、yes会判错。 - 编号与下标错位。城市编号 1 ~ N,数组从下标 1 开始使用,边编号也从 1 开始、用 0 表示空,避免与有效数据冲突。
边界情况
| 场景 | 预期行为 | 说明 |
|---|---|---|
| N = 2,无任何航线(M = 0) | 所有查询 No | 代码对 M = 0 天然兼容 |
| 整张图只有一个 SCC | 所有查询 Yes | 任意两城市均互相可达 |
| 全部城市孤立,无环 | 全部查询 No | 每个城市自成一个 SCC |
| 10^4 个城市连成一条长链 | 链上任意两点查询 No | 同时考验递归深度与单向可达判定 |
| 含重边、平行边 | 结果不受影响 | 邻接表允许重复存储,DFS 标记自动去重 |
| 查询点恰为环上相邻节点 | Yes | 环内任意两点都强连通 |
七、同类变体与延伸
1. SCC 缩点(DAG 上的二次计算)
把每个 SCC 缩成一个点,得到 DAG。缩点后可以:
- 做拓扑排序,统计分量间路径数;
- 求从某个分量出发能到达多少城市;
- 解决经典题《受欢迎的牛》(HAOI2006):缩点后唯一出度为 0 的 SCC 中所有牛被所有牛喜欢,若这样的 SCC 不唯一则答案为 0。
2. 无向版本:连通分量与并查集
若题目改为航线天然双向(每条 u→v 都伴随 v→u),问题退化为无向图连通性,可用并查集近线性解决;单源连通块也可用一次 BFS/DFS 标记。
3. 只要求"单向可达"而非往返
- 小规模图:Floyd 求传递闭包;
- 中大规模:bitset 优化的可达性 DP,或在缩点 DAG 上做拓扑传递;
- 单点出发:一次 DFS/BFS 即可。
4. 带权航线
若航线附带距离、票价、时间:
- 最短/最便宜路径:Dijkstra(非负权)、Bellman-Ford / SPFA(含负权);
- 判定能否往返且总代价最小:先 SCC 内部求环,再在缩点 DAG 上做最短路。
5. 同族经典题
| 题目 | 与本题的联系 |
|---|---|
| 受欢迎的牛 G | SCC 缩点后统计出度 |
| 学校网络 / Network of Schools | 缩点 DAG 上统计入度、出度为 0 的分量数 |
| 飞行路线检查 Flight Routes Check | 同样用 Kosaraju 判断全图是否强连通 |
| 无向图连通块计数 | DFS/BFS 启动次数即答案 |
八、代码实现(C 语言)
版本一:Kosaraju 算法(推荐)
实现原理:第一遍在原图 DFS 并按后序记录完成顺序;第二遍在逆图上按完成顺序逆序 DFS,每次 DFS 树恰好对应一个 SCC。邻接表用全局数组模拟,边编号从 1 开始,0 表示链表结束。
c
#include <stdio.h>
#define MAXN 10005 /* 城市数上限 */
#define MAXE 60005 /* 边数上限:M <= 6N = 60000 */
/* 原图邻接表 */
int head1[MAXN], to1[MAXE], next1[MAXE], edge1;
/* 逆图邻接表 */
int head2[MAXN], to2[MAXE], next2[MAXE], edge2;
int visited[MAXN]; /* 第一遍 DFS 访问标记 */
int order[MAXN], orderCnt; /* 后序完成序列 */
int comp[MAXN]; /* 每个城市所属的 SCC 编号,0 表示未分配 */
/* 同时向原图加 u->v、向逆图加 v->u */
void addEdge(int u, int v)
{
to1[++edge1] = v;
next1[edge1] = head1[u];
head1[u] = edge1;
to2[++edge2] = u;
next2[edge2] = head2[v];
head2[v] = edge2;
}
/* 第一遍:原图 DFS,返回时按后序记录城市 */
void dfs1(int u)
{
visited[u] = 1;
for (int e = head1[u]; e != 0; e = next1[e]) {
int v = to1[e];
if (!visited[v]) {
dfs1(v);
}
}
order[++orderCnt] = u;
}
/* 第二遍:逆图 DFS,把可达城市全部标记为同一个 SCC */
void dfs2(int u, int id)
{
comp[u] = id;
for (int e = head2[u]; e != 0; e = next2[e]) {
int v = to2[e];
if (comp[v] == 0) {
dfs2(v, id);
}
}
}
int main(void)
{
int N, M;
scanf("%d %d", &N, &M);
for (int i = 0; i < M; i++) {
int u, v;
scanf("%d %d", &u, &v);
addEdge(u, v);
}
/* 第一遍 DFS:原图,求后序序列(图可能不连通,需循环启动) */
for (int i = 1; i <= N; i++) {
if (!visited[i]) {
dfs1(i);
}
}
/* 第二遍 DFS:逆图,按完成序列逆序处理,逐个切出 SCC */
int sccCnt = 0;
for (int i = orderCnt; i >= 1; i--) {
int u = order[i];
if (comp[u] == 0) {
dfs2(u, ++sccCnt);
}
}
/* 查询:分量编号相同即互相可达,可以往返 */
int K;
scanf("%d", &K);
while (K--) {
int a, b;
scanf("%d %d", &a, &b);
printf("%s\n", comp[a] == comp[b] ? "Yes" : "No");
}
return 0;
}
版本二:Tarjan 算法(一次 DFS)
实现原理 :维护时间戳 dfn[u](访问次序)和 low[u](u 及子树经回边能到达的最小 dfn)。DFS 路径上的节点压栈;当某节点满足 dfn[u] == low[u] 时,从栈顶弹到 u 的这批节点构成一个 SCC。
c
#include <stdio.h>
#define MAXN 10005
#define MAXE 60005
int head[MAXN], to[MAXE], nxt[MAXE], edgeCnt;
int dfn[MAXN], low[MAXN], timer; /* timer 为全局时间戳 */
int stk[MAXN], inStack[MAXN], top; /* DFS 路径栈及栈内标记 */
int comp[MAXN], sccCnt; /* SCC 编号 */
void addEdge(int u, int v)
{
to[++edgeCnt] = v;
nxt[edgeCnt] = head[u];
head[u] = edgeCnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timer;
stk[++top] = u;
inStack[u] = 1;
for (int e = head[u]; e != 0; e = nxt[e]) {
int v = to[e];
if (dfn[v] == 0) { /* 树边:递归后更新 low */
tarjan(v);
low[u] = low[u] < low[v] ? low[u] : low[v];
} else if (inStack[v]) { /* 指向栈内节点的回边 */
low[u] = low[u] < dfn[v] ? low[u] : dfn[v];
}
}
if (dfn[u] == low[u]) { /* u 是一个 SCC 的根,弹栈 */
sccCnt++;
int x;
do {
x = stk[top--];
inStack[x] = 0;
comp[x] = sccCnt;
} while (x != u);
}
}
int main(void)
{
int N, M;
scanf("%d %d", &N, &M);
for (int i = 0; i < M; i++) {
int u, v;
scanf("%d %d", &u, &v);
addEdge(u, v);
}
for (int i = 1; i <= N; i++) { /* 不连通图:逐点启动 */
if (dfn[i] == 0) {
tarjan(i);
}
}
int K;
scanf("%d", &K);
while (K--) {
int a, b;
scanf("%d %d", &a, &b);
printf("%s\n", comp[a] == comp[b] ? "Yes" : "No");
}
return 0;
}
版本三:并查集(仅适用于"双向航线"变体,不能用于本题)
实现原理 :并查集用 parent 数组表示集合归属,配合路径压缩与按值合并,近常数时间完成连通判定。注意:它只处理无向连通,本题航线有方向,严禁直接套用;此处仅作为对照实现。
c
#include <stdio.h>
#define MAXN 10005
int parent[MAXN];
/* 带路径压缩的查找:沿 parent 上溯,同时把路径节点直接挂到根 */
int find(int x)
{
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
/* 合并两个集合 */
void unite(int a, int b)
{
a = find(a);
b = find(b);
if (a != b) {
parent[a] = b;
}
}
int main(void)
{
int N, M;
scanf("%d %d", &N, &M);
for (int i = 1; i <= N; i++) {
parent[i] = i; /* 初始每个城市各自成集 */
}
for (int i = 0; i < M; i++) {
int u, v;
scanf("%d %d", &u, &v);
/* 仅当 u、v 之间的联系被视为无向(双向)时才允许这样合并 */
unite(u, v);
}
int K;
scanf("%d", &K);
while (K--) {
int a, b;
scanf("%d %d", &a, &b);
printf("%s\n", find(a) == find(b) ? "Yes" : "No");
}
return 0;
}
编译与测试
bash
# 编译并开启全部常用警告
gcc -Wall -Wextra -o routes routes.c
# 运行后粘贴样例输入,逐行得到 Yes / No 判定
./routes