PTA刷题集之Airline Routes(C语言实现)

题目描述(中文翻译)

给定一张航线图,请你判断任意一对城市之间是否可以规划往返行程(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),查询一多必然超时。正确做法是:

  1. 读入全部边后,一次性 求出图中所有 SCC,给每个城市打上分量编号 comp[u];
  2. 每个查询只需比较编号: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 个阶段:

  1. 建表 :用 head / to / next 三个数组模拟邻接表。head[u] 存城市 u 的第一条边编号,to[e] 存边 e 的终点,next[e] 存同一起点的下一条边,边编号 0 表示空;同时维护一套逆图(每条边方向反转)。
  2. 读边:每读入一条 u → v,在原图加 u → v、在逆图加 v → u。
  3. 第一遍 DFS(原图) :对每个未访问城市启动 DFS,在递归返回时 (后序)把城市压入 order,得到按完成时间排列的序列。
  4. 第二遍 DFS(逆图) :按 order 从后往前取城市,若尚未分配分量则在逆图上 DFS,本次 DFS 到达的所有城市属于同一个 SCC,统一写入分量编号。
  5. 处理查询 :读入 K,逐条读入城市对,比较分量编号,用三元运算符选择输出 Yes 或 No。
  6. 结束 :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 限制。

六、易错点与边界情况

易错点

  1. 把有向图当成无向图。航线有方向,只有 a→b 而没有回程路径时不能往返;这是本题最根本的概念点。
  2. 误用并查集 。并查集只能表达"无向连通",无法表达有向可达,直接 union(u, v) 会把大量单向可达的城市错误判为 Yes。
  3. 每个查询临时 DFS/BFS。总复杂度随 K 膨胀,是本题主要的超时原因;必须先离线求 SCC。
  4. 第一遍 DFS 顺序记录错误 。城市必须在递归返回之后 (后序)写入 order,写成访问时立即记录(前序)会导致第二遍切分错误。
  5. 第二遍忘记换逆图或顺序搞反 。必须在逆图上、按完成序列从后往前处理,三个条件缺一不可。
  6. 忘记图可能不连通 。两遍 DFS 都要套一层 for (i = 1; i <= N; i++) 循环,从每个未访问城市重新启动。
  7. 边数组开小导致越界。M 最大为 6N = 60000,正图、逆图各需容纳 M 条边,容量按 60005 开。
  8. 递归深度风险。图为一条长链时 DFS 递归深度可达 10^4 层。本题规模下默认栈通常可承受;若环境栈很小,需改写为显式栈的迭代 DFS。
  9. 输出大小写不符 。必须严格输出 Yes / No,写成 YES、yes 会判错。
  10. 编号与下标错位。城市编号 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
相关推荐
宗旨飞翔4 小时前
不同的二叉搜索树
算法
damoluomu4 小时前
Node 的 Headless 出厂没有前台,Java 的 Headless 是从门户里长出来的
java·开发语言·node.js
736c4 小时前
固件开发岗算法
数据结构·算法
我命由我123454 小时前
Python 实用操作 - 文件操作(打开与关闭文件、读取文件、文件写入)
java·开发语言·前端·javascript·数据库·python·java-ee
青少儿编程课堂4 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:倍增法求最近公共祖先LCA解析
c++·python·算法·bfs·信息学竞赛
冻柠檬飞冰走茶4 小时前
PTA刷题集之Battle Over Cities - Hard Version(C语言实现)
c语言·开发语言·数据结构·算法
吴长建先生重名了4 小时前
电商订单系统在Docker和Kubernetes中部署的高
算法
Дерек的学习记录4 小时前
Unreal Engine 5:第三人称运动系统和人物动画
开发语言·学习·ue5
Ramble_Naylor5 小时前
Rust深度解读:异步流(Async Streams)与生成器的处理艺术
开发语言·后端·rust