1. 题目来源
链接:815. 公交路线
相似:[最短路] aw920. 最优乘车(单源最短路建图+bfs最短路模型+知识理解+好题)
2. 题目解析
好题,之前做过,但似懂非懂。
问题抽象:
每一个公交线路都是一个环,如果两环有公共点,则这两个环相连。
本题是求最少经过几个公交车,从起点 S
到达终点 T
,即等价于从起点经过最少的环到达终点。
则可以将每个环看成一个点,其公共点就代表两点之间有边相连。在此依据题意,边为无向边。
故,在此边权为 1,求起点到终点的最短距离,那么就是 bfs
最短路模型了。
建图:
- 如何建立点边之间的关系是本题建图一大难点,方法也有很多,在此具体实现如下:
- 将环抽象成点。
- 环相连,等价于两环中存在公共点,则一个公共点提供一条边。
- 由于起点可能是多个环的公共点,且从任意一个环出发都是合理的,所以本题是多源
bfs
问题,需要将起点所在环全部入队。 bfs
一开始存的为起点所在的环编号,然后遍历起点所在环内的所有点,其中这些点有可能通向其它的环,如果其它的环还未被拓展到的话,那么说明从该环到另一个环的最短路就被找到了。就需要将该点所通到的其他所有环全部加入队列,进行遍历各个环中的所有点。bfs
最短路,第一次遍历到的一定是最短的。可以拿距离判断,也可以拿非法值来判断。即针对dist
数组有两种初始化方式,dist[i]=-1
或dist[i]=1e9
,前者在更新时仅需判断dist[y]==-1
即可判断环y
是否是第一次更新,后者需要判断距离,即dist[y]>dist[x]+1
这个也是象征性的判断,因为不存在多次更新的情况,bfs
第一次遍历第一次更新一定是最小的。- 在遍历每个环中的各个点时,顺便判断终点。
- 在遍历每个环中的各个点时,bfs 操作下,第一次遇到该点就会将它的所有出边全部进行拓展,且第一次遇见该点时即为最短的距离。所以,拓展完该点的信息后,就需要将该点进行删除,避免重复遍历。
注意点:
- bfs 拓展好处是:第一次遍历到的点,一定是最短的,且由该点进行下一步更新信息时,信息也是完全的。所以该点不应该再被二次遍历,是没有实际意义的,也不可能是最短路的所处路径。故,为了避免重复遍历,可以将遍历过的点直接进行删除。两者性能差距如下:
- 这个建图方式挺需要注意的。将环看成一个个最短路点,环中包含所有的环内节点,环1 走到 环2 时才去更新最短路,路径+1。每次将环内的所有节点拿出来,看看这些节点有没有关联到其他环,关联到的话,就进行距离更新,再将下一个环入队,去看它环内的所有节点。
时间复杂度: O ( n + m ) O(n+m) O(n+m),每个点只会被遍历一次
空间复杂度: O ( n ) O(n) O(n)
标程:
class Solution {
public:
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
if (source == target) return 0; // 起点终点相同
int n = routes.size();
unordered_map<int, vector<int>> g;
vector<int> dist(n, 1e9);
queue<int> q;
// 建图
for (int i = 0; i < n; i ++ ) {
for (auto x : routes[i]) {
if (x == source) { // 多源,初始化、入队
dist[i] = 1;
q.push(i); // 若起点在多个环上,则这些环都能当做起点使用,故是多源问题
}
g[x].push_back(i); // 每个点所在的环形公交路线
}
}
// bfs
while (q.size()) {
auto t = q.front(); q.pop();
for (auto x : routes[t]) {
if (x == target) return dist[t];
for (auto y : g[x]) {
if (dist[y] > dist[t] + 1) {
dist[y] = dist[t] + 1;
q.push(y);
}
}
g.erase(x);
}
}
return -1;
}
};
详细注释版:
class Solution {
public:
int numBusesToDestination(vector<vector<int>>& routes, int source, int target) {
if (source == target) return 0; // 不可少,题目要求
int n = routes.size();
unordered_map<int, vector<int>> g;
queue<int> q;
vector<int> dist(n, -1); // 采用 -1 表示未被 bfs 遍历过
for (int i = 0; i < n; i ++ ) {
for (auto e : routes[i]) {
if (e == source) { // 起点所在环入队,环距离初始化为 1
q.push(i);
dist[i] = 1;
}
g[e].push_back(i); // 各个点存放其所关联到的所有的环
}
}
while (q.size()) {
auto t = q.front(); q.pop();
for (auto e : routes[t]) { // 遍历当前环中所有的点
if (e == target) return dist[t]; // 如果等于目标点,则找到答案,且由于 bfs 性质,一定为最短路
for (auto ne : g[e]) { // 遍历这些点的其他关联的环,更新距离
if (dist[ne] == -1) { // 如果 ne 环没有被遍历过
dist[ne] = dist[t] + 1; // 则说明可以从 e 点走到 ne 环,距离加1
q.push(ne); // ne 环入队,下一次遍历它的所有的环中节点
}
}
// 优化点,可有可无。加上性能提高一倍
g.erase(e); // 如果点 e 已经被遍历过,那么点 e 的距离、关联环都已经被更新,信息已经全部获取到了,避免重复遍历,直接从哈希表中删除。
}
}
return -1;
}
};