LeetCode - Medium - 332. Reconstruct Itinerary

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 and toi consist of uppercase English letters.
  • fromi != toi

Analysis

方法一:回溯算法

待解決問題

  1. 一個行程中,如果航班處理不好容易變成一個圈,成爲死循環
  2. 有多種解法,字母序靠前排在前面,如何該記錄映射關係呢 ?
  3. 使用回溯法(也可以說深搜) 的話,那麼終止條件是什麼呢?
  4. 搜索的過程中,如何遍歷一個機場所對應的所有機場。

如何理解死循環

對於死循環,舉一個有重複機場的例子:

舉這個例子是爲了說明出發機場和到達機場也會重複的,如果在解題的過程中沒有對集合元素處理好,就會死循環。

該記錄映射關係

字母序靠前排在前面,如何該記錄映射關係呢 ?

一個機場映射多個機場,機場之間要靠字母序排列,一個機場映射多個機場,可以使用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.

參考資料

  1. 回溯算法:重新安排行程
  2. 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 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());
	}
	
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章