问题:给我们一个公交线路列表,列表的每条线路routes[i]是一个表示停靠站序号的数组,每条线路都是循环线路。例如,如果routes[0]=[1, 5, 7],这条线路停靠站的顺序是1->5->7->1>5->7->1…。如果从序号为S的站出发,想去序号为T的站,请问至少坐几条线路的公交车才能到达目的地。如果不可能到达目的地,返回-1。例如,routes = [[1, 2, 7], [3, 6, 7]],S = 1,T = 6,那么我们坐公交从站1到站7,然后转公交坐到站6,因此需要乘坐2条线路的公交。
分析:这是LeetCode的第815题。
很多跟乘坐交通工具相关的面试题都是关于图的问题。不管是坐飞机、还是坐汽车或者火车,通常停靠的站可以看成是图的顶点。如果能从一个站到另一个站,那么这两个站对应在图中顶点有一条边相连。
如果一个问题是关于图的,那么很有可能是考察图的搜索算法。如果问题是求最短距离的,优先考虑广度优先搜索;如果问题是求一条符合某条件的路径,优先考虑深度优先搜索。这个题目求最少需要乘坐几条线路的公交车,是最短距离的变形,也可以应用广度优先搜索解决。
广度优先搜索可以看成是按层在图上搜索,从源点出发,先搜索距离源点为1的所有顶点,再搜索距离源点为2的所有顶点,以此类推。在本题中,距离不简单是顶点与顶点之间的距离。由于本题是求需要乘坐公交车线路的最小数目。如果两个站在同一条公交线路中,它们之间是不需要转车的,因此我们可以把它们的距离看成是0。如果两个站需要转一次车才能到达,那么它们之间的距离为1。
通常我们可以用队列(Queue)来做广度优先搜索。为了方便求得最短距离(本题为最少乘坐公交的线路),我们可以用两个队列。如果队列queue1里放的是乘坐n条公交线路能到达的站,那么从queue1里面的站再转一次车能到达的站(即乘坐n+1条公交线能到达的站)放到队列queue2里。交替这两个队列做广度优先搜索,直到到达目的地为止。
这种思路对应的代码如下:
public int numBusesToDestination(int[][] routes, int S, int T) {
if (S == T) {
return 0;
}
Map> stopToRoutes = new HashMap<>();
for (int i = 0; i < routes.length; ++i) {
for (int j = 0; j < routes[i].length; ++j) {
int stop = routes[i][j];
if (!stopToRoutes.containsKey(stop)) {
stopToRoutes.put(stop, new HashSet());
}
stopToRoutes.get(stop).add(i);
}
}
Queue queue1 = new LinkedList<>();
Queue queue2 = new LinkedList<>();
Set visited = new HashSet<>();
int steps = 1;
enqueueNeighbors(routes, S, stopToRoutes, queue1, visited);
while (!queue1.isEmpty()) {
int stop = queue1.poll();
if (stop == T) {
return steps;
}
enqueueNeighbors(routes, stop, stopToRoutes, queue2, visited);
if (queue1.isEmpty()) {
queue1 = queue2;
queue2 = new LinkedList<>();
steps++;
}
}
return -1;
}
上述代码中,哈希表stopToRoutes记录每个车站的公交线路序号。由于一个站有可能多条线路经过,因此需要一个Set来记录多个公交线路序号。
每次从queue1里拿出一个站stop,表示最少乘坐n条公交线路可以到达这个站。如果有多条线经过这个站并且这些线路之前还没有坐过,那么从stop站再转一次车就能到了,因此可以把从stop站转车能到达的站都放进queue2里。这就是下面enqueueNeighbors的作用。
最开始的时候我们从S出发,和S处在同一条公交线路上的其他站都不需要转车就能到达,因此初始化的时候都可以放进队列queue1里。这就是在广度优先搜索的while循环之前也要调用enqueueNeighbors的原因。
函数enqueueNeighbors如下所示:
private void enqueueNeighbors(int[][] routes, int stop,
Map> stopToRoutes,
Queue queue, Set visited) {
for (int route: stopToRoutes.get(stop)) {
for (int s : routes[route]) {
if (!visited.contains(s)) {
queue.offer(s);
visited.add(s);
}
}
}
}
广度优先搜索搜索的时间复杂度是O(V + E),其中V是图中顶点的数目,E是图中边的数目。
更多算法面试题的讨论,欢迎访问博客http://qingyun.io/blogs。