用Java搞定LeetCode 815公交路线题:BFS建图思路详解(附完整代码)
当你面对LeetCode 815公交路线问题时,是否曾被如何将公交线路和车站转化为图结构所困扰?这道题看似简单,实则暗藏玄机——它考察的不仅是对BFS算法的理解,更是对现实问题抽象为图论模型的能力。本文将带你从零开始,一步步拆解这道经典题目,掌握"问题抽象→图建模→算法实现"的完整解题链条。
1. 问题本质与建模思路
公交路线问题表面上是一个路径搜索问题,但它的特殊之处在于:我们需要在两个不同维度之间建立联系——公交线路和车站。传统的BFS通常处理的是单一维度的节点关系,而这道题要求我们同时考虑"车站"和"线路"这两个层级的关系。
理解这个问题的关键在于认识到:
- 车站是物理存在的站点
- 线路是连接车站的服务单元
- 换乘意味着从一个线路切换到另一个线路
这种双层关系可以用图论中的二分图来建模:
- 一类节点代表公交线路
- 另一类节点代表车站
- 边表示"某线路包含某车站"的关系
然而,直接使用这种建模方式会导致图结构过于复杂。更聪明的做法是:
java复制// 建立车站到线路的映射关系
Map<Integer, List<Integer>> stationToRoutes = new HashMap<>();
for (int i = 0; i < routes.length; i++) {
for (int station : routes[i]) {
stationToRoutes.computeIfAbsent(station, k -> new ArrayList<>()).add(i);
}
}
这种建模方式的优势在于:
- 保留了所有必要信息
- 避免了构建显式的图结构
- 便于快速查询任意车站对应的线路
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法设计与实现
基于上述建模,我们可以设计一个双层BFS算法:
2.1 算法框架
- 初始化:检查起点和终点是否相同
- 预处理:构建车站到线路的映射
- BFS准备:将起点车站的所有线路加入队列
- BFS执行:逐层扩展可到达的线路
- 终止条件:找到包含目标车站的线路
2.2 关键数据结构
java复制// 车站到线路的映射
Map<Integer, List<Integer>> stationToRoutes = new HashMap<>();
// BFS队列
Queue<Integer> queue = new LinkedList<>();
// 记录已访问的线路
boolean[] visited = new boolean[routes.length];
2.3 BFS核心逻辑
java复制while (!queue.isEmpty()) {
int size = queue.size();
level++;
for (int i = 0; i < size; i++) {
int route = queue.poll();
for (int station : routes[route]) {
if (station == target) {
return level;
}
for (int nextRoute : stationToRoutes.get(station)) {
if (!visited[nextRoute]) {
visited[nextRoute] = true;
queue.offer(nextRoute);
}
}
}
}
}
3. 优化技巧与边界处理
在实际编码中,有几个关键点需要特别注意:
3.1 性能优化
- 提前终止:找到目标立即返回
- 访问标记:避免重复处理同一线路
- 层级计数:准确记录换乘次数
3.2 边界情况
- 起点终点相同:直接返回0
- 不可达情况:返回-1
- 单线路直达:返回1
3.3 复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 建图 | O(S) | O(S) |
| BFS | O(R + S) | O(R) |
其中R是线路数量,S是车站总数。
4. 完整代码实现
以下是经过优化的完整Java解决方案:
java复制import java.util.*;
class Solution {
public int numBusesToDestination(int[][] routes, int source, int target) {
if (source == target) return 0;
// 建立车站到线路的映射
Map<Integer, List<Integer>> stationToRoutes = new HashMap<>();
for (int i = 0; i < routes.length; i++) {
for (int station : routes[i]) {
stationToRoutes.computeIfAbsent(station, k -> new ArrayList<>()).add(i);
}
}
// BFS初始化
Queue<Integer> queue = new LinkedList<>();
boolean[] visited = new boolean[routes.length];
for (int route : stationToRoutes.getOrDefault(source, new ArrayList<>())) {
queue.offer(route);
visited[route] = true;
}
int level = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int route = queue.poll();
for (int station : routes[route]) {
if (station == target) return level;
for (int nextRoute : stationToRoutes.get(station)) {
if (!visited[nextRoute]) {
visited[nextRoute] = true;
queue.offer(nextRoute);
}
}
}
}
level++;
}
return -1;
}
}
5. 实战技巧与常见误区
在解决这类问题时,开发者常会遇到几个典型问题:
- 建模错误:试图直接对车站建图,忽略线路维度
- 层级混淆:错误计算换乘次数
- 性能陷阱:未合理剪枝导致超时
避免这些问题的实用技巧:
- 双重循环检查:外层遍历线路,内层遍历车站
- 明确层级边界:在BFS中正确处理层级计数
- 合理使用数据结构:选择适合的Map和Queue实现
6. 扩展思考与应用场景
掌握这种建图技巧后,你可以轻松应对类似问题:
- 地铁换乘规划:原理完全相同
- 航班中转计算:将航线视为"公交线路"
- 社交网络分析:用户和群组的双层关系
这种建模思路的价值在于它揭示了一个通用模式:当问题涉及两个不同维度的实体时,考虑建立它们之间的映射关系,而非直接连接同维度实体。
