题目

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次只能用一次

示例 1:
image.png

  1. 输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
  2. 输出:["JFK","MUC","LHR","SFO","SJC"]

示例 2:
image.png

输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromi 和 toi 由大写英文字母组成
  • fromi != toi

    解题方法

    递归回溯+哈希表

    使用哈希表记录机票信息,具体如下:
    unordered_map<string, map<string, int>> targets;
    // <出发机场,<到达机场,机票数量>>
    
    使用map可以对同一机场出发的机票进行排序,再通过递归回溯穷举所有机票连接可能。
    C++代码: ```cpp class Solution { private: unordered_map> targets; vector result; int total_num = 0;

public: bool backtracking(int num) { if(num==total_num) return true;

    string start = result[result.size()-1];
    for(pair<const string, int>& target : targets[start]) {
        if(target.second) {
            result.push_back(target.first);
            target.second--;
            if(backtracking(num+1)) return true;
            result.pop_back();
            target.second++;
        }
    }

    return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
    total_num = tickets.size();
    for(const vector<string>& vec : tickets) {
        targets[vec[0]][vec[1]]++;
    }
    result.push_back("JFK");
    backtracking(0);
    return result;
}

};

<a name="AjAj7"></a>
## Hierholzer 算法
本体旨在求解 **欧拉通路/欧拉回路**,与一笔画类似。因题目保证至少存在一种可行解,故题目给出的是一张**欧拉图**或**半欧拉图**。关于欧拉图和半欧拉图的定义如下:

   - 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路;
   - 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路;
   - 具有欧拉回路的无向图称为欧拉图;
   - 具有欧拉通路但不具有欧拉回路的无向图称为半欧拉图。

如果题目没有保证至少存在一种可行解,则需要自行判断是否为欧拉图或半欧拉图:

   - 对于无向图 `G`,`G` 是欧拉图当且仅当 `G` 是连通的且没有奇度顶点。
   - 对于无向图 `G`,`G` 是半欧拉图当且仅当 `G` 是连通的且 `G` 中恰有 `0` 个或 `2` 个奇度顶点。
   - 对于有向图 `G`,`G` 是欧拉图当且仅当 `G` 的所有顶点属于同一个强连通分量且每个顶点的入度和出度相同。
   - 对于有向图 `G`,`G` 是半欧拉图当且仅当
      - 如果将 `G` 中的所有有向边退化为无向边时,那么 `G` 的所有顶点属于同一个强连通分量;
      - 最多只有一个顶点的出度与入度差为 `1`;
      - 最多只有一个顶点的入度与出度差为 `1`;
      - 所有其他顶点的入度和出度相同。

由于题目还要求输出字典序最小的结果,因此构建图时需要对每条边按字典序排序。

**Hierholzer 算法**用于在连通图中寻找欧拉路径,其流程如下:

   1. 从起点出发,进行深度优先搜索。
   1. 每次沿着某条边从某个顶点移动到另外一个顶点的时候,都需要删除这条边。
   1. 如果没有可移动的路径,则将所在节点加入到栈中,并返回。

当我们顺序地考虑该问题时,我们也许很难解决该问题,因为我们无法判断当前节点的哪一个分支是「死胡同」分支。

不妨倒过来思考。我们注意到只有那个入度与出度差为 `1` 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。我们可以改变入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。

对于当前节点而言,从它的每一个非「死胡同」分支出发进行深度优先搜索,都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。

这样就能保证我们可以「一笔画」地走完所有边,最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。

时间复杂度:`O(mlogm)`,空间复杂度:`O(m)`。其中 `m` 是边的数量。对于每一条边我们需要 `O(logm)` 地删除它,最终的答案序列长度为 `m+1`,而与 `n` 无关。

**C++代码:**
```cpp
class Solution {
public:
    unordered_map<string, map<string, int>> targets;
    int total_num = 0;
    vector<string> result;

    void backtracking(const string& curr) {
        for(pair<const string, int>& target : targets[curr]) {
            if(target.second) {
                string tmp = target.first;
                target.second--;
                backtracking(move(tmp));
            }
        }
        result.push_back(curr);
    }

    vector<string> findItinerary(vector<vector<string>>& tickets) {
        total_num = tickets.size();
        for (vector<string>& vec : tickets) {
            targets[vec[0]][vec[1]]++;
        }
        backtracking("JFK");

        reverse(result.begin(), result.end());
        return result;
    }
};