公交路线 图解题解
这道题到底在问什么
- 输入
- routes=[[1,2,7],[3,6,7],[6,8,9]], source=1, target=9
- 输出
- 3(R0 → R1 → R2)
最优解:一步一步想明白
- 3记住「建站→线路映射、再以线路为节点 BFS,层数就是乘车数」,下面逐帧套它。
- 4先把 3 条线路各画成一个节点:R0=[1,2,7]、R1=[3,6,7]、R2=[6,8,9],此刻还没有任何边。下面逐站扫描,谁和谁共享站点,就在谁和谁之间连一条「可换乘」的边。
- 5处理 R0 的站 1:把 R0 记到「站 1」名下。目前只有它一条线路经过这站,先记着。
- 6处理 R0 的站 2:把 R0 记到「站 2」名下。目前只有它一条线路经过这站,先记着。
- 7处理 R0 的站 7:把 R0 记到「站 7」名下。目前只有它一条线路经过这站,先记着。
- 8处理 R1 的站 3:把 R1 记到「站 3」名下。目前只有它一条线路经过这站,先记着。
- 9处理 R1 的站 6:把 R1 记到「站 6」名下。目前只有它一条线路经过这站,先记着。
- 10线路 R1 经过站 7,而站 7 之前已经被 R0 记下了。这说明在站 7 可以从 R0 换乘到 R1,于是给 R0 和 R1 连一条边(高亮)。
- 11线路 R2 经过站 6,而站 6 之前已经被 R1 记下了。这说明在站 6 可以从 R1 换乘到 R2,于是给 R1 和 R2 连一条边(高亮)。
- 12处理 R2 的站 8:把 R2 记到「站 8」名下。目前只有它一条线路经过这站,先记着。
- 13处理 R2 的站 9:把 R2 记到「站 9」名下。目前只有它一条线路经过这站,先记着。
- 14映射建好了:站 7 把 R0、R1 连起来,站 6 把 R1、R2 连起来。现在这张「线路图」就成形了,接下来在它上面做 BFS,数从起点线路到含 target 的线路要走几步。
- 15起点是站 1。先查映射:经过站 1 的线路是 R0,把它们入队、标记为已坐(乘车数从 1 起步)。BFS 第一层就是「坐第 1 辆车能到的线路」。
- 16弹出队首 R0(紫色),它是坐第 1 辆车能到的线路之一。先看终点站 9 在不在它停靠的站里。
- 17终点站 9 不在 R0 里。通过共享站换乘到新线路 R1(站 7),把它们入队、标记已坐。 R0 这条线路本轮处理完(变绿)。
- 18第 1 辆车能到的线路都查完了,还没碰到终点。乘车数加到 2,继续处理队列里下一层的线路。
- 19弹出队首 R1(紫色),它是坐第 2 辆车能到的线路之一。先看终点站 9 在不在它停靠的站里。
- 20终点站 9 不在 R1 里。通过共享站换乘到新线路 R2(站 6),把它们入队、标记已坐。 R1 这条线路本轮处理完(变绿)。
- 21第 2 辆车能到的线路都查完了,还没碰到终点。乘车数加到 3,继续处理队列里下一层的线路。
- 22弹出队首 R2(紫色),它是坐第 3 辆车能到的线路之一。先看终点站 9 在不在它停靠的站里。
- 23R2 停靠的站 [6,8,9] 里有终点站 9(变绿)!说明坐到第 3 辆车就能抵达,答案就是 3。BFS 第一次碰到含 target 的线路,层数即最少乘车数。
⚠️ 容易写错的地方
✗ 错:把站点当成 BFS 的节点
✓ 对:把线路当节点,共享站才是换乘边
一条线路一上车就能直达它的所有站,坐一辆车的代价相同,所以「换乘一次」对应「换一条线路」,层数才等于乘车数
✗ 错:忘了 source 等于 target 的特例
✓ 对:先判 source 等于 target 直接返回 0
已经在终点站、一辆车都不用坐;不特判会从 1 起步多算一辆
✗ 错:用线路下标去重时漏标 seen
✓ 对:入队即标 seen、绝不重复入队
多条线路共享多个站,不去重会让同一线路反复入队、甚至死循环
完整代码(Python / C++ / Java)
Python
from typing import List
from collections import defaultdict, deque
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
if source == target:
return 0
stop_to_routes = defaultdict(list)
for i, route in enumerate(routes):
for stop in route:
stop_to_routes[stop].append(i)
q = deque()
seen_routes = set()
for r in stop_to_routes[source]:
q.append(r)
seen_routes.add(r)
buses = 1
while q:
for _ in range(len(q)):
r = q.popleft()
if target in routes[r]:
return buses
for stop in routes[r]:
for nr in stop_to_routes[stop]:
if nr not in seen_routes:
seen_routes.add(nr)
q.append(nr)
buses += 1
return -1C++
#include <queue>
#include <unordered_map>
#include <unordered_set>
#include <vector>
using namespace std;
class Solution {
public:
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
if (source == target) return 0;
unordered_map<int, vector<int>> stopToRoutes;
for (int i = 0; i < (int)routes.size(); ++i) for (int stop : routes[i]) stopToRoutes[stop].push_back(i);
queue<int> q;
vector<int> seen(routes.size());
for (int r : stopToRoutes[source]) { q.push(r); seen[r] = 1; }
for (int buses = 1; !q.empty(); ++buses) {
for (int sz = q.size(); sz--; ) {
int r = q.front(); q.pop();
for (int stop : routes[r]) {
if (stop == target) return buses;
for (int nr : stopToRoutes[stop]) if (!seen[nr]) {
seen[nr] = 1; q.push(nr);
}
}
}
}
return -1;
}
};Java
import java.util.*;
class Solution {
public int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) return 0;
Map<Integer, List<Integer>> stopToRoutes = new HashMap<>();
for (int i = 0; i < routes.length; i++) for (int stop : routes[i]) stopToRoutes.computeIfAbsent(stop, k -> new ArrayList<>()).add(i);
Queue<Integer> q = new ArrayDeque<>();
boolean[] seen = new boolean[routes.length];
for (int r : stopToRoutes.getOrDefault(source, Collections.emptyList())) { q.offer(r); seen[r] = true; }
for (int buses = 1; !q.isEmpty(); buses++) {
for (int sz = q.size(); sz > 0; sz--) {
int r = q.poll();
for (int stop : routes[r]) {
if (stop == target) return buses;
for (int nr : stopToRoutes.get(stop)) if (!seen[nr]) {
seen[nr] = true; q.offer(nr);
}
}
}
}
return -1;
}
}复杂度
时间
O(∑Sᵢ) ~ 最坏二次
建映射要扫所有线路的所有站,合计是各线路长度之和 ∑Sᵢ;BFS 每条线路最多入队一次,但展开它时要遍历它每个站、再看该站经过的所有线路,高频站会被多条线路重复扫到。本实现没有清空已用站,最坏会按各站线路数的平方汇总、退到二次级;若在站被首次展开后清空它的线路表,就能压到 ∑Sᵢ 线性
空间
O(∑Sᵢ)
stopToRoutes 映射存下每个站对应的线路、合计 ∑Sᵢ;seen 与队列最多装下所有线路
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 公交路线 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么不直接在站点图上跑 BFS 求最短路?+
可以建站点图,但一条线路内部所有站两两可达(上一辆车就全到了),它们之间的边权应为 0、不同线路换乘才算 1,这是 0-1 最短路、写起来更绕;而且还得给「上第一辆车」补上 1 的代价、或额外记录当前在哪条线路,否则起点和终点同在一条线路时会被误算成 0(正确答案是 1)。把线路抽象成节点后,换乘恰好是普通的单位边、第一层就是第 1 辆车,直接套标准 BFS、层数就是乘车数,更干净。
建 stopToRoutes 映射相比每次现找有什么好处?+
若不建映射,每次想知道「某个站还能换哪些线路」都要扫一遍所有线路,BFS 里反复扫会退化。预处理出「站 → 经过它的线路」后,换乘查询变成 O(1) 取列表,免去每次全量扫描所有线路;不过本实现展开站后没清空它的线路表,高频站仍可能被多条线路重复扫到,想做到严格线性可在站首次展开后清空它的列表。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 公交路线 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。