Topic
- Depth-first Search
- Graph
Description
https://leetcode.com/problems/reconstruct-itinerary/
You are given a list of airline tickets
where tickets[i] = [fromi, toi]
represent the departure and the arrival airports of one flight. Reconstruct the itinerary in order and return it.
All of the tickets belong to a man who departs from "JFK"
, thus, the itinerary must begin with "JFK"
. 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"]
.
You may assume all tickets form at least one valid itinerary. You must use all the tickets once and only once.
Example 1:
Input: tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
Output: ["JFK","MUC","LHR","SFO","SJC"]
Example 2:
Input: tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
Output: ["JFK","ATL","JFK","SFO","ATL","SFO"]
Explanation: Another possible reconstruction is ["JFK","SFO","ATL","JFK","ATL","SFO"] but it is larger in lexical order.
Constraints:
1 <= tickets.length <= 300
tickets[i].length == 2
fromi.length == 3
toi.length == 3
fromi
andtoi
consist of uppercase English letters.fromi != toi
Analysis
方法一:回溯算法
待解決問題
- 一個行程中,如果航班處理不好容易變成一個圈,成爲死循環
- 有多種解法,字母序靠前排在前面,如何該記錄映射關係呢 ?
- 使用回溯法(也可以說深搜) 的話,那麼終止條件是什麼呢?
- 搜索的過程中,如何遍歷一個機場所對應的所有機場。
如何理解死循環
對於死循環,舉一個有重複機場的例子:
舉這個例子是爲了說明出發機場和到達機場也會重複的,如果在解題的過程中沒有對集合元素處理好,就會死循環。
該記錄映射關係
字母序靠前排在前面,如何該記錄映射關係呢 ?
一個機場映射多個機場,機場之間要靠字母序排列,一個機場映射多個機場,可以使用HashMap
,如果讓多個機場之間再有順序的話,就用TreeMap
。
接口Map<String, Map<String, Integer>>
,具體實現類HashMap<String, TreeMap<String, Integer>>
,具體含義Map<出發機場, Map<到達機場, 航班次數>> flight
。
在遍歷Map<出發機場, Map<到達機場, 航班次數>> flight
的過程中,使用"航班次數"這個字段的數字做相應的增減,來標記到達機場是否使用過了,避免死鎖問題。
如果“航班次數”大於零,說明目的地還可以飛,如果如果“航班次數”等於零說明目的地不能飛了,而不用對集合做刪除元素或者增加元素的操作。
回溯三弄
回溯算法模板:
void backtracking(參數) {
if (終止條件) {
存放結果;
return;
}
for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {
處理節點;
backtracking(路徑,選擇列表); // 遞歸
回溯,撤銷處理結果
}
}
本題以輸入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]爲例,抽象爲樹形結構如下:
遞歸函數參數
List<String> path
:路程,也就最終返回結果。int numOfTickets
:票數,在終止條件處有用。Map<String, Map<String, Integer>> flight
:具體含義Map<出發機場, Map<到達機場, 航班次數>> flight
。
代碼如下:
private boolean backtacking(List<String> path, int startIndex, int numOfTickets, Map<String, Map<String, Integer>> flight) {}
注意函數返回值是boolean,而不是大多數的返回值void。
因爲只需要找到一個行程,就是在樹形結構中唯一的一條通向葉子節點的路線,便立即返回,如圖:
當然本題的flight和path都需要初始化,代碼如下:
List<String> path = new ArrayList<>();
Map<String, Map<String, Integer>> flight = ticket2Flight(tickets);
path.add("JFK");//加入起始地址
// 用到Java8的流水線和收集器的功能。
private Map<String, Map<String, Integer>> ticket2Flight(List<List<String>> tickets) {
return tickets.stream().collect(Collectors.groupingBy(ticket -> ticket.get(0), //groupingBy()的默認實現Map是HashMap
Collectors.groupingBy(ticket -> ticket.get(1), TreeMap::new, //
Collectors.reducing(0, elem -> 1, Integer::sum))));
}
遞歸終止條件
拿題目中的示例爲例,輸入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,這是有4個航班,那麼只要找出一種行程,行程裏的機場個數是5就可以了。
所以終止條件是:我們回溯遍歷的過程中,遇到的機場個數,如果達到了(航班數量+1),那麼我們就找到了一個行程,把所有航班串在一起了。
代碼如下:
if (path.size() == numOfTickets + 1) {
return true;
}
單層搜索的邏輯
直接放碼吧!
Map<String, Integer> terminal2NumMap = flight.get(path.get(path.size() - 1));
if (terminal2NumMap != null) {
for (Entry<String, Integer> terminal2Num : terminal2NumMap.entrySet()) {
if (terminal2Num.getValue() > 0) {
terminal2Num.setValue(terminal2Num.getValue() - 1);
path.add(terminal2Num.getKey());
if (backtacking(path, numOfTickets, flight)) {
return true;
}
path.remove(path.size() - 1);
terminal2Num.setValue(terminal2Num.getValue() + 1);
}
}
}
方法二:DFS
All the airports are vertices and tickets are directed edges. Then all these tickets form a directed graph.
The graph must be Eulerian since we know that a Eulerian path exists.
Thus, start from "JFK", we can apply the Hierholzer's algorithm to find a Eulerian path in the graph which is a valid reconstruction.
Since the problem asks for lexical order smallest solution, we can put the neighbors in a min-heap. In this way, we always visit the smallest possible neighbor first in our trip.
參考資料
Submission
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.PriorityQueue;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class ReconstructItinerary {
//方法一:回溯算法
public List<String> findItinerary(List<List<String>> tickets) {
List<String> path = new ArrayList<>();
Map<String, Map<String, Integer>> flight = ticket2Flight(tickets);
path.add("JFK");
backtacking(path, tickets.size(), flight);
return path;
}
private boolean backtacking(List<String> path, int numOfTickets,
Map<String, Map<String, Integer>> flight) {
if (path.size() == numOfTickets + 1) {
return true;
}
Map<String, Integer> terminal2NumMap = flight.get(path.get(path.size() - 1));
if (terminal2NumMap != null) {
for (Entry<String, Integer> terminal2Num : terminal2NumMap.entrySet()) {
if (terminal2Num.getValue() > 0) {
terminal2Num.setValue(terminal2Num.getValue() - 1);
path.add(terminal2Num.getKey());
if (backtacking(path, numOfTickets, flight)) {
return true;
}
path.remove(path.size() - 1);
terminal2Num.setValue(terminal2Num.getValue() + 1);
}
}
}
return false;
}
// Java 8
private Map<String, Map<String, Integer>> ticket2Flight(List<List<String>> tickets) {
return tickets.stream().collect(Collectors.groupingBy(ticket -> ticket.get(0), //groupingBy()的默認實現Map是HashMap
Collectors.groupingBy(ticket -> ticket.get(1), TreeMap::new, //
Collectors.reducing(0, elem -> 1, Integer::sum))));
}
//方法二:DFS
public List<String> findItinerary2(List<List<String>> tickets) {
Map<String, PriorityQueue<String>> flights = new HashMap<>();
List<String> path = new LinkedList<>();
for (List<String> ticket : tickets) {
flights.computeIfAbsent(ticket.get(0), k -> new PriorityQueue<>()).add(ticket.get(1));
}
dfs("JFK", path, flights);
return path;
}
private void dfs(String departure, List<String> path, Map<String, PriorityQueue<String>> flights) {
PriorityQueue<String> arrivals = flights.get(departure);
while (arrivals != null && !arrivals.isEmpty())
dfs(arrivals.poll(), path, flights);
path.add(0, departure);
}
}
Test
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.Test;
public class ReconstructItineraryTest {
private ReconstructItinerary obj = new ReconstructItinerary();
private List<List<String>> tickets = Arrays.asList(Arrays.asList("MUC", "LHR"), Arrays.asList("JFK", "MUC"), //
Arrays.asList("SFO", "SJC"), Arrays.asList("LHR","SFO"));
private List<List<String>> tickets2 = Arrays.asList(Arrays.asList("JFK", "SFO"), Arrays.asList("JFK", "ATL"), //
Arrays.asList("SFO", "ATL"), Arrays.asList("ATL","JFK"), Arrays.asList("ATL", "SFO"));
private List<List<String>> tickets3 = Arrays.asList(Arrays.asList("JFK","KUL"), Arrays.asList("JFK","NRT"), //
Arrays.asList("NRT","JFK"));
private List<String> expected = Arrays.asList("JFK", "MUC", "LHR", "SFO", "SJC");
private List<String> expected2 = Arrays.asList("JFK", "ATL", "JFK", "SFO", "ATL", "SFO");
private List<String> expected3 = Arrays.asList("JFK", "NRT", "JFK", "KUL");
@Test
public void test() {
assertThat(obj.findItinerary(tickets), is(expected));
assertThat(obj.findItinerary(tickets2), is(expected2));
assertThat(obj.findItinerary(tickets3), is(expected3));
}
@Test
public void test2() {
assertThat(obj.findItinerary2(tickets), is(expected));
assertThat(obj.findItinerary2(tickets2), is(expected2));
assertThat(obj.findItinerary2(tickets3), is(expected3));
}
@Test
public void testTicket2Flight() {
Map<String, Map<String, Integer>> ticket2Flight = invokeMethod(obj, "ticket2Flight", tickets);
assertEquals("{LHR={SFO=1}, MUC={LHR=1}, SFO={SJC=1}, JFK={MUC=1}}", ticket2Flight.toString());
Map<String, Map<String, Integer>> ticket2Flight2 = invokeMethod(obj, "ticket2Flight", tickets2);
assertEquals("{ATL={JFK=1, SFO=1}, SFO={ATL=1}, JFK={ATL=1, SFO=1}}", ticket2Flight2.toString());
}
}