Given a list of airline tickets represented by pairs of departure and arrival airports [from, to]
, reconstruct the itinerary in order. All of the tickets belong to a man who departs from JFK
. Thus, the itinerary must begin with JFK
.
Note:
- If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string. For example, the itinerary
["JFK", "LGA"]
has a smaller lexical order than["JFK", "LGB"]
. - All airports are represented by three capital letters (IATA code).
- You may assume all tickets form at least one valid itinerary.
题意很简单,抽象出来的话,就是从一个给定点出发,访问所有边且每条边只访问一次,也即欧拉路径问题。
直接的想法就是用DFS:当无路可走但是边还没有访问完时就“回退”,直到找到一条欧拉路径。想法很简单,但实现的过程中遇到诸多问题。
class Solution {
private:
int edge_num = 0;
public:
vector<string> findItinerary(vector<pair<string, string>> tickets) {
this->edge_num = tickets.size();
map<string,vector<string>> adj_list;
for(const auto&i : tickets) {
adj_list[i.first].push_back(i.second); //提升效率
}
for(auto &i:adj_list)
sort(i.second.begin(), i.second.end());
vector<string> result = {"JFK"};
string node = "JFK";
int pass = 0;
DFS(result, node, adj_list, pass);
return result;
}
void DFS(vector<string>& result, string node, map<string,vector<string>>& adj_list, int& pass) {
if(adj_list[node].size() == 0) {
return;
}
int size = adj_list[node].size();
for(auto i = adj_list[node].begin();i != adj_list[node].end();) {
if(size == 0)
break;
size--; //为了确定当前遍历的大小,如果DFS失败,edge会被重新添加回当前邻接链表中
string new_node = *i;
result.push_back(*i);
adj_list[node].erase(i); //之后i将不可用,已经时后面的值了
pass++;
DFS(result, new_node, adj_list, pass);
if(pass == this->edge_num)
return;
auto end = result.end();
result.erase(--end);
adj_list[node].push_back(new_node);
pass--;
}
}
};
首先,创建邻接链表。
选择什么形式的STL容器比较合适?(由于STL容器用的不熟练,每次使用都要重复查询,之后得进行总结。)因为题目里有要求一定的顺序,那就使用map<string, set<string>>吧。但实际上,当访问失败要回退时,要将边重新加入map中,此时set又会进行一次排序,导致我们又再选择不正确的路径。所以不能用set,就换成vector吧,初始时进行排序就行了。
在创建过程中有一个技巧,按理来说,添加路径时,邻接链表的head应该先建立才能对它进行增添操作。所以就需要判断head是否已经创建。但实际上不用这样做,map已经帮我们做好了。(这一优化从beat 10%上升到beat 70%)
然后就是DFS递归部分。
返回条件:无边可选或者已经访问完所有的边。然后对当前结点的所有可选的路径进行处理,选择一条路径,将它从邻接链表删去,如果递归下去的结果访问完所有边,结束。如果没有,就不选择这条路径,并将它加回邻接链表(递归看着易懂,自己想的时候就不一定了)。需要注意的是,这个方法不好,在循环的过程中对正在循环的对象进行了增删操作,容易出bug。
我们再来看看其他人的优雅的代码,使用的是Hierholzer算法。
class Solution {
unordered_map<string, priority_queue<string, vector<string>, greater<string>>> graph;
vector<string> result;
void dfs(string vtex) {
auto & edges = graph[vtex];
while (!edges.empty())
{
string to_vtex = edges.top();
edges.pop();
dfs(to_vtex);
}
result.push_back(vtex);
}
public:
vector<string> findItinerary(vector<pair<string, string>> tickets) {
for (auto e : tickets)
graph[e.first].push(e.second);
dfs("JFK");
reverse(result.begin(), result.end());
return result;
}
};
欧拉路径、回路的一些相关定理:
定理1:(a) 如果一个图G有超过两个奇数度的点,那么G没有欧拉路径
(b) 如果G是连通的且有两个奇数度的点,那么G中存在一个欧拉路径。任何G中的欧拉路径都必须从一个奇数度点开始并结束于另一奇数度点。
定理2:如果图G是连通图且所有结点都有偶数的度,那么G中存在欧拉回路
题目给定起始点为“JFK”,所以终点要么是另一个奇数度的点,要么就是“JFK”。所以第一次“无边可走”时,到达了终点,最后一次“无边可走”时,到达了起点。将结果翻转之后就是答案。