LeetCode - Medium中Reconstruct Itinerary的示例分析
这篇文章给大家介绍LeetCode - Medium中Reconstruct Itinerary的示例分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
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
,具体实现类HashMap
,具体含义Map<出发机场, Map<到达机场, 航班次数>> flight
。
在遍历Map<出发机场, Map<到达机场, 航班次数>> flight
的过程中,使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了,避免死锁问题。
如果"航班次数"大于零,说明目的地还可以飞,如果如果"航班次数"等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
回溯三弄
回溯算法模板:
void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 }}
本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下:
递归函数参数
List
:路程,也就最终返回结果。path int numOfTickets
:票数,在终止条件处有用。Map
:具体含义> flight Map<出发机场, Map<到达机场, 航班次数>> flight
。
代码如下:
private boolean backtacking(Listpath, int startIndex, int numOfTickets, Map > flight) {}
注意函数返回值是boolean,而不是大多数的返回值void。
因为只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,便立即返回,如图:
当然本题的flight和path都需要初始化,代码如下:
Listpath = new ArrayList<>();Map > flight = ticket2Flight(tickets);path.add("JFK");//加入起始地址// 用到Java8的流水线和收集器的功能。private Map > ticket2Flight(List > 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;}
单层搜索的逻辑
直接放码吧!
Mapterminal2NumMap = flight.get(path.get(path.size() - 1));if (terminal2NumMap != null) { for (Entry 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.
参考资料
回溯算法:重新安排行程
Share my solution
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 ListfindItinerary(List > tickets) { List
path = new ArrayList<>(); Map > flight = ticket2Flight(tickets); path.add("JFK"); backtacking(path, tickets.size(), flight); return path; } private boolean backtacking(List path, int numOfTickets, Map > flight) { if (path.size() == numOfTickets + 1) { return true; } Map terminal2NumMap = flight.get(path.get(path.size() - 1)); if (terminal2NumMap != null) { for (Entry 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 > ticket2Flight(List > 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
findItinerary2(List > tickets) { Map
> flights = new HashMap<>(); List path = new LinkedList<>(); for (List 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 path, Map > flights) { PriorityQueue 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> tickets = Arrays.asList(Arrays.asList("MUC", "LHR"), Arrays.asList("JFK", "MUC"), // Arrays.asList("SFO", "SJC"), Arrays.asList("LHR","SFO")); private List
> 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
> tickets3 = Arrays.asList(Arrays.asList("JFK","KUL"), Arrays.asList("JFK","NRT"), // Arrays.asList("NRT","JFK")); private List
expected = Arrays.asList("JFK", "MUC", "LHR", "SFO", "SJC"); private List expected2 = Arrays.asList("JFK", "ATL", "JFK", "SFO", "ATL", "SFO"); private List 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 > ticket2Flight = invokeMethod(obj, "ticket2Flight", tickets); assertEquals("{LHR={SFO=1}, MUC={LHR=1}, SFO={SJC=1}, JFK={MUC=1}}", ticket2Flight.toString()); Map > ticket2Flight2 = invokeMethod(obj, "ticket2Flight", tickets2); assertEquals("{ATL={JFK=1, SFO=1}, SFO={ATL=1}, JFK={ATL=1, SFO=1}}", ticket2Flight2.toString()); } }
关于LeetCode - Medium中Reconstruct Itinerary的示例分析就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。