LeetCode-05-BFS和DFS
阿新 • • 發佈:2020-12-21
第五講 BFS與DFS
5.1 介紹
- BFS和DFS是兩種最常見的優先搜尋演算法,廣泛應用於圖和樹等資料結構的搜尋中
- 什麼是DFS
- 在遍歷的時候,就認準一條路走到黑,不撞南牆不回頭。撞了南牆之後再回頭找別的路
- 什麼是BFS
- 在進行遍歷的時候,每遇到一個“分岔路口”,先隨便選一個,記錄沒選的,然後繼續向下走,直到走不下去了。然後在回過頭來找最近的“分岔路口”
- 什麼是回溯法
- 回溯法(backtracking)是優先搜尋的一種特殊情況,又稱為試探法,常用於需要記錄節點狀態的深度優先搜尋。通常來說,排列、組合、選擇類問題使用回溯法比較方便
- 回溯法的核心就是回溯,要還原所有被修改的東西
5.2 DFS
695. 島嶼的最大面積(中等)
-
分析
- 對於圖中的任意一個不為0的點,需要搜尋的是這個點的四個方向。同時為了保證不重複計算面積,我們需要將已經訪問過的點至為0,這樣就保證了在計算面積的時候不會重複計算
- 時間 O(n), 空間 O(n),其中
n
是地圖中所有點的數量
-
題解
public int maxAreaOfIsland(int[][] grid) { if (grid.length == 0) { return 0; } int maxArea = 0; for (int i = 0; i < grid.length; i++) { for (int j = 0; j < grid[0].length; j++) { int area = dfs(i, j, grid); maxArea = Math.max(maxArea, area); } } return maxArea; } private int dfs(int i, int j, int[][] grid) { // 這裡要注意判斷時的順序 if (i < 0 || j < 0 || i == grid.length || j == grid[0].length || grid[i][j] == 0) { return 0; } int size = 1; grid[i][j] = 0; // 這個點的面積 S = s上 + s下 + s左 + s右 size += dfs(i, j - 1, grid) + dfs(i, j + 1, grid) + dfs(i - 1, j, grid) + dfs(i + 1, j, grid); return size; }
547. 朋友圈(中等)
-
分析
- 這道題和上一道題目有異曲同工之妙,類似於上一題中計算島嶼的數量
- 同樣要注意對已經搜尋過朋友圈的同學進行標記
- 時間 O(n), 空間 O(n),其中
n
是同學個數
-
題解
public int findCircleNum(int[][] M) { if (M.length == 0) { return 0; } int n = 0; boolean[] flag = new boolean[M.length]; for (int i = 0; i < M.length; i++) { // 如果同學i沒有搜尋過,則搜尋同學i if (!flag[i]) { // 標記同學i已經被搜尋 flag[i] = true; dfs(i, M, flag); n++; } } return n; } private void dfs(int i, int[][] M, boolean[] flag) { // 搜尋同學i的朋友圈 for (int k = 0; k < M.length; k++) { if (!flag[k] && M[i][k] == 1) { flag[k] = true; dfs(k, M, flag); } } }
417. 太平洋大西洋水流問題(中等)
-
分析
- 對於一個陸地單元來說,如果水流可以從這裡同時流動到兩個大洋,那麼這個單元必定在兩個大洋的交界處。或者說可以通過其他單元流動到兩個海洋
- 如果說我們對於每一個點都進行一次判斷,看他是否能走到兩個大洋的話,會出現不少重複的判斷。
- 所以我們可以考慮倒著來。從海洋出發,然後看看那些點可以到達,這樣的話就可以避免重複判斷。(這裡有點動態規劃的意思,單元
i
能否留到海洋,由他上下左右四個方向上的單元 和 其能否到達這四個方向上的單元共同決定) - 時間 O(n), 空間 O(n),其中
n
是陸地單元個數
-
題解
public static List<List<Integer>> pacificAtlantic(int[][] matrix) { if (matrix.length == 0) { return null; } List<List<Integer>> res = new ArrayList<>(); int n = matrix.length; int m = matrix[0].length; boolean[][] toPacific = new boolean[n][m]; boolean[][] toAtlantic = new boolean[n][m]; // 從兩個海洋出發,進行搜尋, for (int i = 0; i < n; i++) { dfs(matrix, toPacific, i, 0); dfs(matrix, toAtlantic, i, m - 1); } for (int i = 0; i < m; i++) { dfs(matrix, toPacific, 0, i); dfs(matrix, toAtlantic, n - 1, i); } // 取交集 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (toAtlantic[i][j] && toPacific[i][j]) { res.add(Arrays.asList(i, j)); } } } return res; } /** * @param matrix 矩陣 * @param flag 狀態表 * @param i 縱座標 * @param j 橫座標 */ private static void dfs(int[][] matrix, boolean[][] flag, int i, int j) { if (i < 0 || j < 0 || i == matrix.length || j == matrix[0].length || flag[i][j]) { return; } flag[i][j] = true; // 因為我們倒過來進行搜尋,要能流過濾,也就是說要滿足這個點的高度小於等於某個方向上的高度,這樣才能流過來 if (i > 0 && matrix[i][j] <= matrix[i - 1][j]) { dfs(matrix, flag, i - 1, j); } if (i < matrix.length - 1 && matrix[i][j] <= matrix[i + 1][j]) { dfs(matrix, flag, i + 1, j); } if (j > 0 && matrix[i][j] <= matrix[i][j - 1]) { dfs(matrix, flag, i, j - 1); } if (j < matrix[0].length - 1 && matrix[i][j] <= matrix[i][j + 1]) { dfs(matrix, flag, i, j + 1); } }
5.3 回溯法
46. 全排列(中等)
-
分析
- 全排列是一個典型的回溯演算法
- 我們先往棧裡面放一個數,然後標記一個這個數已經被使用過了,然後對剩下的繼續搜尋。當搜尋完所有的數之後,我們將當前棧中的內容做個記錄。然後丟擲棧頂元素,還原狀態,再繼續搜尋
- 遞迴終點:棧內的元素個數 等於
nums
中的元素個數 - 時間 O(n), 空間 O(n),其中
n
是元素個數
-
題解
public List<List<Integer>> permute(int[] nums) { List<List<Integer>> res = new ArrayList<>(); if (nums.length == 0) { return res; } int n = nums.length; boolean[] flag = new boolean[n]; Deque<Integer> stack = new ArrayDeque<>(3); dfs(nums, flag, stack, res, n); return res; } /** * @param nums 需要全排列的陣列 * @param flag 狀態陣列,記錄是否訪問過該元素 * @param stack 記錄單次排列結果 * @param res 記錄所有的排列結果 * @param n 元素個數 */ private void dfs(int[] nums, boolean[] flag, Deque<Integer> stack, List<List<Integer>> res, int n) { // 遞迴終點 if (stack.size() == n) { // 為了防止新增的是引用而不是整個棧,所以這裡直接建立一個新的物件即可 res.add(new ArrayList<>(stack)); return; } for (int i = 0; i < n; i++) { // 如果這個元素沒有被訪問過,那麼訪問,然後遞迴呼叫。遞迴結束之後,還原狀態變數 if (!flag[i]) { flag[i] = true; stack.addLast(nums[i]); dfs(nums, flag, stack, res, n); flag[i] = false; stack.removeLast(); } } }
79. 單詞搜尋(中等)
-
分析
- 回溯演算法
-
題解
public static boolean exist(char[][] board, String word) { if (board.length == 0) { return false; } boolean[][] flag = new boolean[board.length][board[0].length]; for (int i = 0; i < board.length; i++) { for (int j = 0; j < board[0].length; j++) { System.out.println("i = " + i + ", j = " + j); if (dfs(board, word, flag, 0, i, j)) { return true; } } } return false; } /** * @param board 字母表 * @param word 單次 * @param flag 標記陣列 * @param k 單詞搜尋到的位置 * @param i 橫座標 * @param j 縱座標 * @return 查詢結果 */ private static boolean dfs(char[][] board, String word, boolean[][] flag, int k, int i, int j) { if (k == word.length()) { return true; } if (i < 0 || j < 0 || i == board.length || j == board[0].length || flag[i][j]) { return false; } if (word.charAt(k) != board[i][j]) { return false; } // 向上下左右四個方向去搜索 flag[i][j] = true; boolean up = dfs(board, word, flag, k + 1, i - 1, j); boolean down = dfs(board, word, flag, k + 1, i + 1, j); boolean left = dfs(board, word, flag, k + 1, i, j - 1); boolean right = dfs(board, word, flag, k + 1, i, j + 1); flag[i][j] = false; return up || down || left || right; }
-
分析
- 當board中全部是word的字元時,會超時,原因是以為遞迴很容易超時。上述給出的演算法,時間複雜度為O(mn x 4^L),其中,
m
,n
,L
分別為字元表的長、寬和word的長度。顯而易見的,我們需要進行一些剪枝。
- 當board中全部是word的字元時,會超時,原因是以為遞迴很容易超時。上述給出的演算法,時間複雜度為O(mn x 4^L),其中,
-
優化
-
參考官方題解給出的解決,優化程式碼結構如下
private static boolean dfs(char[][] board, String word, boolean[][] flag, int k, int i, int j) { if (word.charAt(k) != board[i][j]) { return false; } if (k == word.length() - 1) { return true; } // 向上下左右四個方向去搜索 flag[i][j] = true; int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; boolean result = false; for (int[] dir : directions) { int newi = i + dir[0]; int newj = j + dir[1]; if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) { if (!flag[newi][newj]) { boolean p = dfs(board, word, flag, k + 1, newi, newj); if (p) { result = true; break; } } } } flag[i][j] = false; return result; }
-
51. N 皇后(困難)
-
分析
- 逐行安排皇后,同時標記不能安排的地方。檢查下一行,如果下一行沒地方,則回溯
- 時間 O(n!), 空間 O(n),其中
n
是皇后個數
-
題解
public static List<List<String>> solveNQueens(int n) { List<List<String>> res = new ArrayList<>(); if (n == 0) { return res; } // 建立棋盤 List<char[]> map = new ArrayList<>(n); for (int i = 0; i < n; i++) { char[] p = new char[n]; Arrays.fill(p, '.'); map.add(p); } // 建立訪問陣列 boolean[][] flag = new boolean[n][n]; NQueens(res, map, flag, 0, n); return res; } /** * @param res 結果集 * @param map 棋盤 * @param flag 位置陣列 * @param index 第幾行的皇后需要安排 * @param n 皇后的數量 */ private static void NQueens(List<List<String>> res, List<char[]> map, boolean[][] flag, int index, int n) { if (index == n) { List<String> list = new ArrayList<>(); for (char[] chars : map) { list.add(new String(chars)); } res.add(list); return; } // 對在index行上的皇后依次安排 for (int i = 0; i < n; i++) { if (flag[index][i]) { continue; } map.get(index)[i] = 'Q'; List<int[]> temp = setFlag(n, flag, index, i); NQueens(res, map, flag, index + 1, n); // 把上次修改的改回來。思考為什麼不能直接複用setFlag函式? for (int[] p : temp) { flag[p[0]][p[1]] = false; } map.get(index)[i] = '.'; } } /** * 記錄新增皇后後修改的節點。這裡要注意,原有不可放置的地方不能被記錄 * 因為恢復的時候要按照這裡記錄的資訊進恢復 * * @param n 皇后的數量 * @param flag 狀態表 * @param row 新增皇后的行號 * @param column 新增行號的列號 * @return 新增皇后之後,修改的狀態表節點 */ private static List<int[]> setFlag(int n, boolean[][] flag, int row, int column) { List<int[]> res = new ArrayList<>(); for (int nr = row + 1; nr < n; nr++) { for (int nc = 0; nc < n; nc++) { if (flag[nr][nc]) { continue; } if (nc == column || nc + nr == column + row || nc - nr == column - row) { flag[nr][nc] = true; res.add(new int[]{nr, nc}); } } } return res; }
-
重點
-
為什麼在新增皇后之後需要記錄狀態表修改的位置?
-
假設我們在安排第
i
行的皇后,成功安排好了。然後去安排第i+1
行的皇后。在對該行進行試探的時候,如果某處試探失敗,我們需要回溯,把狀態表改回原來的樣子。那麼如果直接根據位置進行修改,可能會影響到第i
行皇后放置後增加的修改條件。 -
舉例
- 如圖所示,紅色標記的是第1行皇后所在位置的限制,綠色表示的是安排第二行之後新增的位置限制
- 因為我們採取逐行進行試探,所以不需要記錄同行中的變化
- 假如綠色三角位置的皇后試探失敗,
- 如果我們根據綠色三角的位置進行清除,清除行,清除對角線,清除列。會刪除到紅色位置的標記,這就使得紅色三角的限制不完善。
- 所以我們要記錄綠色的位置,然後根據綠色的位置進行還原。(這就是回溯法的精髓所在,只改上一次改的)
-
5.4 BFS
934. 最短的橋(中等)
-
分析
- 要架橋,那麼就要找到這兩個島。於是先進行一次BFS,找到兩個島,同時記錄這個島上的所有點。
- 然後選擇一個島,從這個島出發,進行BFS搜尋,尋找到另外一個島的路徑長度,返回最短的路徑長度
- 時間 O(MN), 空間 O(MN),其中
M
和N
是地圖的長和寬
-
程式碼
public int shortestBridge(int[][] A) { Queue<int[]> queue = new LinkedList<>(); Queue<int[]> island = new LinkedList<>(); int[][] direction = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; int row = A.length; int column = A[0].length; // 找到第一個島 boolean sign = false; for (int i = 0; i < row; i++) { if (sign) { break; } for (int j = 0; j < column; j++) { if (A[i][j] == 1) { queue.offer(new int[]{i, j}); // bfs(queue, island, A, direction); dfs(island, A, i, j); sign = true; break; } } } // bfs查詢另外一個島 int len = -1; while (!island.isEmpty()) { int size = island.size(); len++; while (size-- > 0) { int[] p = island.poll(); for (int[] i : direction) { int newR = p[0] + i[0]; int newC = p[1] + i[1]; if (newR >= 0 && newR < row && newC >= 0 && newC < column) { if (A[newR][newC] == 2) { continue; } if (A[newR][newC] == 1) { return len; } A[newR][newC] = 2; island.offer(new int[]{newR, newC}); } } } } return len; } private void dfs(Queue<int[]> island, int[][] map, int row, int column) { if (row < 0 || column < 0 || row == map.length || column == map[0].length || map[row][column] == 2 || map[row][column] == 0) { return; } if (map[row][column] == 1) { map[row][column] = 2; island.offer(new int[]{row, column}); } dfs(island, map, row - 1, column); dfs(island, map, row + 1, column); dfs(island, map, row, column - 1); dfs(island, map, row, column + 1); } private void bfs(Queue<int[]> queue, Queue<int[]> island, int[][] map, int[][] direction) { while (!queue.isEmpty()) { int[] p = queue.poll(); island.offer(p); int r = p[0]; int c = p[1]; map[r][c] = 2; for (int[] i : direction) { int newR = r + i[0]; int newC = c + i[1]; if (newR >= 0 && newR < map.length && newC >= 0 && newC < map[0].length && map[newR][newC] == 1) { queue.offer(new int[]{newR, newC}); } } } }
-
分析:
-
我的思路是進行兩次BFS,提交之後發現超時了。不明所以,自測的時候發現並不是進入死迴圈,時間大概在3秒左右。
-
翻看題解,發現題解採用了DFS。嘗試使用了DFS,自測時間發現時間大大縮短,因此不明所以。如果有大佬知道為什麼的話,還望解惑
-
126. 單詞接龍 II(困難)
-
分析
- 看到最短就要想到BFS,看到BFS就要想到圖。由於沒有給出明確的圖的模型,所以我們需要將題目中的要求抽象成圖,然後進行BFS搜尋
- 如果兩個單詞之間可以進行相互轉換,那麼我們就認為這兩個單詞相連,由此可以抽象出一個無向圖
- 在得到圖之後,BFS搜尋找到最短路徑的長度即可
- 在得到最短路徑之後,我們在用回溯法去搜索
- 時間 O(N^2 x C), 空間 O(N^2),其中
N
為單詞表的長度,C
為單詞表中單詞的長度
-
程式碼
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) { List<List<String>> res = new ArrayList<>(); if (beginWord.length() == 1) { res.add(Arrays.asList(beginWord, endWord)); return res; } Map<String, Set<String>> map = new HashMap<>(); generate(map, beginWord, wordList); if (!map.containsKey(endWord)) { return res; } // bfs找到最短路徑長度 Set<String> visited = new HashSet<>(); Deque<String> path = new ArrayDeque<>(); int step = bfs(beginWord, endWord, map, path, visited); System.out.println(step); // dfs回溯搜尋路徑 List<List<String>> allPath = new ArrayList<>(); path.clear(); visited.clear(); dfs(res, beginWord, endWord, map, path, visited, step); return res; } public void dfs(List<List<String>> res, String beginWord, String endWord, Map<String, Set<String>> map, Deque<String> stack, Set<String> visited, int step) { if (beginWord.equals(endWord) && stack.size() == step - 1) { stack.addLast(beginWord); res.add(new ArrayList<>(stack)); stack.removeLast(); return; } if (visited.contains(beginWord) || stack.size() >= step) { return; } visited.add(beginWord); stack.addLast(beginWord); for (String s : map.get(beginWord)) { dfs(res, s, endWord, map, stack, visited, step); } visited.remove(beginWord); stack.removeLast(); } public int bfs(String beginWord, String endWord, Map<String, Set<String>> map, Deque<String> queue, Set<String> visited) { queue.add(beginWord); visited.add(beginWord); int len = 1; int size = queue.size(); while (size-- > 0 && !queue.isEmpty()) { String current = queue.removeFirst(); Set<String> set = map.get(current); for (String s : set) { if (visited.contains(s)) { continue; } if (s.equals(endWord)) { return len + 1; } queue.addLast(s); visited.add(s); } if (size == 0) { size = queue.size(); len++; } } return 0; } public void generate(Map<String, Set<String>> map, String beginWord, List<String> wordList) { List<String> list = new ArrayList<>(wordList); list.add(beginWord); for (String s1 : list) { Set<String> set = new HashSet<>(); for (String s2 : list) { if (check(s1, s2)) { set.add(s2); } } map.put(s1, set); } } public boolean check(String word1, String word2) { if (word1.equals(word2)) { return false; } int p = 0; for (int i = 0; i < word1.length(); i++) { if (word1.charAt(i) != word2.charAt(i)) { p++; } } return p < 2; }
-
分析:超時。當圖比較複雜時,時間消耗主要集中在DFS回溯搜尋上。
-
優化:在BFS搜尋的時候,記錄路徑。然後一起返回
private static void bfs2(List<List<String>> res, String beginWord, String endWord, Map<String, Set<String>> map, Set<String> visited) { // 把beginword放到路徑中,然後放到佇列中 Deque<Deque<String>> queue = new ArrayDeque<>(); Deque<String> p = new LinkedList<>(); p.add(beginWord); visited.add(beginWord); queue.add(p); int size = queue.size(); int len = 1; // 因為bfs進行搜尋的時候,第一次找到的一定是最短的路徑,此時記錄最短路徑長度 // 再找到最短的之後,就沒有必要在繼續向下一層去搜索了,只要橫向搜尋即可 int min = map.size() + 1; while(size-- > 0 && !queue.isEmpty() && min >= len) { // 出隊,獲得原路徑上最後一個點,再從這個點開始進行搜尋 Deque<String> path = queue.removeFirst(); String last = path.getLast(); for(String s : map.get(last)) { if (visited.contains(s)) { continue; } if(s.equals(endWord) && min >= len) { path.addLast(s); min = len; res.add(new ArrayList<>(path)); continue; } Deque<String> nextPath = new ArrayDeque<>(path); nextPath.addLast(s); queue.addLast(nextPath); } if (size == 0) { size = queue.size(); len++; // 在這裡記錄這層中被訪問過的元素 for (Deque<String> strings : queue) { visited.addAll(strings); } } } }
5.5 基礎練習
257. 二叉樹的所有路徑(簡單)
-
分析
- 很簡單的回溯演算法
- 時間 O(N^2), 空間 O(N^2),其中
N
樹的節點數量
-
程式碼
public List<String> binaryTreePaths(TreeNode root) { List<String> res = new ArrayList<>(); if(root == null) { return res; } Stack<TreeNode> stack = new Stack<>(); stack.push(root); dfs(res, stack, root); return res; } private void dfs(List<String> res, Stack<TreeNode> stack, TreeNode node) { // TODO Auto-generated method stub if(node.left == null && node.right == null) { StringBuilder sb = new StringBuilder(); for(int i = 0; i < stack.size(); i++) { if (i != 0) { sb.append("->"); } sb.append(stack.get(i).val); } res.add(sb.toString()); } if(node.left != null) { stack.push(node.left); dfs(res, stack, node.left); stack.pop(); } if(node.right != null) { stack.push(node.right); dfs(res, stack, node.right); stack.pop(); } }
39. 組合總和(中等)
-
分析
- 回溯演算法
- 時間 O(S), 空間 O(target),其中
S
為所有可行解的長度之和
-
程式碼
public List<List<Integer>> combinationSum(int[] candidates, int target) { List<List<Integer>> res = new ArrayList<>(); Deque<Integer> stack = new ArrayDeque<>(); Arrays.sort(candidates); dfs(res, stack, candidates, 0, target); return res; } private void dfs(List<List<Integer>> res, Deque<Integer> stack, int[] nums, int current, int target) { // TODO Auto-generated method stub if (current == target) { List<Integer> list = new ArrayList<>(stack); Collections.sort(list); for(List<Integer> l : res) { if (list.equals(l)) { return; } } res.add(list); return; } for(int i : nums) { if(current + i > target) { return; } stack.addLast(i); dfs(res, stack, nums, current + i, target); stack.removeLast(); } }
-
缺點:簡單剪枝的回溯演算法,效率較低
130. 被圍繞的區域(中等)
-
分析
- 因為任何與邊界相連的O都不會被填充為X,那麼我們就從邊界開始尋找。找到所有不會被填充的位置,然後填充除了這些位置之外的位置
- 時間 O(MN), 空間 O(MN),其中
M
和N
是地圖的長和寬
-
程式碼
public void solve(char[][] board) { if (board.length == 0) { return; } boolean[][] flag = new boolean[board.length][board[0].length]; int row = board.length; int coloum = board[0].length; // 先搜尋上下邊界的點 for (int i = 0; i < coloum; i++) { if (board[0][i] == 'O' && !flag[0][i]) { dfs(board, flag, 0, i); } if (board[row - 1][i] == 'O' && !flag[row - 1][i]) { dfs(board, flag, row - 1, i); } } // 再搜尋左右邊界的點 for (int i = 0; i < row; i++) { if (board[i][0] == 'O' && !flag[i][0]) { dfs(board, flag, i, 0); } if (board[i][coloum - 1] == 'O' && !flag[i][coloum - 1]) { dfs(board, flag, i, coloum - 1); } } for (int i = 0; i < row; i++) { for (int j = 0; j < coloum; j++) { if (board[i][j] != 'X' && !flag[i][j]) { board[i][j] = 'X'; } } } } private void dfs(char[][] board, boolean[][] flag, int r, int c) { if (board[r][c] != 'O') { return; } flag[r][c] = true; int[][] pos = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; for (int i = 0; i < 4; i++) { int nr = r + pos[i][0]; int nc = c + pos[i][1]; if (nr >= 0 && nc >= 0 && nr < board.length && nc < board[0].length && !flag[nr][nc]) { dfs(board, flag, nr, nc); } } }
5.6 進階練習
47. 全排列 II(中等)
-
分析
- 這個和普通的全排列問題不一樣。區別在於本題給出的元素中包含重複的數字,這樣就會導致可能存在重複的結果。關鍵就是怎麼把結果去重
- 時間 O(n x n!), 空間 O(n),其中
n
是元素個數。因為對於n
個數,全排列的結果數是n!
,每次在去重的時候,都要遍歷已經排列出來的所有組合
-
程式碼
public List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> res = new ArrayList<>(); Deque<Integer> stack = new ArrayDeque<>(nums.length); boolean[] flag = new boolean[nums.length]; dfs(res, nums, stack, flag); return res; } private void dfs(List<List<Integer>> res, int[] nums, Deque<Integer> stack, boolean[] flag) { if(stack.size() == nums.length) { List<Integer> list = new ArrayList<>(stack); for(List<Integer> l : res) { if (l.equals(list)) { return; } } res.add(list); return; } for(int i = 0; i < nums.length; i++) { if(!flag[i]) { stack.addLast(nums[i]); flag[i] = true; dfs(res, nums, stack, flag); stack.removeLast(); flag[i] = false; } } }
-
問題:在完成一次全排列之後進行查重。這樣可能會進行多次無用的全排列,從而提高耗時
40. 組合總和 II(中等)
-
分析
- 依舊是回溯法,由於存在重複的數字,且不允許給出重複的解,所以要進行去重
- 常用的而且比較顯而易見的剪枝方法是將
candidates
排序,然後在去排列組合。對於給出的current
如果已經大於target
,則直接返回
-
程式碼
public List<List<Integer>> combinationSum2(int[] candidates, int target) { List<List<Integer>> res = new ArrayList<>(); Arrays.sort(candidates); int sum = 0; for(int i : candidates) { sum += i; } if(sum < target) { return res; } Deque<Integer> stack = new ArrayDeque<>(); boolean[] visited = new boolean[candidates.length]; dfs(res, stack, visited, candidates, 0, target); return res; } private void dfs(List<List<Integer>> res, Deque<Integer> stack, boolean[] flag, int[] nums, int current, int target) { if (current == target) { List<Integer> list = new ArrayList<>(stack); Collections.sort(list); for(List<Integer> l : res) { if (l.equals(list)) { return; } } res.add(list); return; } for(int i = 0; i< nums.length; i++) { if(current + nums[i] <= target && !flag[i]) { stack.addLast(nums[i]); flag[i] = true; dfs(res, stack, flag, nums, current + nums[i], target); stack.removeLast(); flag[i] = false; } else { return; } } }
310. 最小高度樹(中等)(有缺陷,不完善)
-
分析
- 轉換圖的儲存方式,使用hashmap儲存圖,key是節點編號,value是相連的節點編號
- 要找最小高度的數,而且最小高度的數可能不只一顆,所以很容易就想到了遍歷所有節點,然後找到高度最小樹。
- 時間 O(n^2), 空間 O(n),
-
程式碼
public static List<Integer> findMinHeightTrees(int n, int[][] edges) { List<Integer> res = null; if(n <= 2) { res = new ArrayList<>(); for(int i = 0; i < n; i++) { res.add(i); } return res; } // 把給出的邊分類做記錄 Map<Integer, List<Integer>> map = new HashMap<>(); set(map, n, edges); System.out.println(map); // key:高度,value:根節點序號 Map<Integer, List<Integer>> hmap = new HashMap<>(); for(int i = 0; i < n; i++) { boolean[] flag = new boolean[n]; int h = bfs(map, i, flag); List<Integer> list = hmap.getOrDefault(h, new ArrayList<>()); list.add(i); hmap.put(h, list); } System.out.println(hmap); for (int i = 0; i < n; i++) { if (hmap.containsKey(i)) { res = hmap.get(i); break; } } return res; } private static int bfs(Map<Integer, List<Integer>> map, int n, boolean[] flag) { Deque<Integer> queue = new ArrayDeque<>(); queue.addLast(n); int size = queue.size(); int h = 0; flag[n] = true; while(size-- > 0 && !queue.isEmpty()) { List<Integer> list = map.get(queue.removeFirst()); for(int i : list) { if (!flag[i]) { queue.addLast(i); flag[i] = true; } } if(size == 0) { size = queue.size(); h++; } } return h; } private static void set(Map<Integer, List<Integer>> map, int n, int[][] edges) { for(int[] i : edges) { List<Integer> list1 = map.getOrDefault(i[0], new ArrayList<Integer>()); list1.add(i[1]); map.put(i[0], list1); List<Integer> list2 = map.getOrDefault(i[1], new ArrayList<Integer>()); list2.add(i[0]); map.put(i[1], list2); } }
-
缺點:
- 對於每一顆樹,都要進行 O(n) 級別的計算高度。這在樹的數量非常多的時候會浪費很多時間。
- 不過我們可以優化計算高度的過程,如果出現了較小的高度,那麼我們在BFS計算高度的時候,如果目前高度已經超過了這個較小的高度,那麼我們可以直接終止這次計算,這樣的話能減少額外計算高度的代價
-
優化
private static int bfs(Map<Integer, List<Integer>> map, int n, boolean[] flag, int minH) { Deque<Integer> queue = new ArrayDeque<>(); queue.addLast(n); int size = queue.size(); int h = 0; flag[n] = true; while (size-- > 0 && !queue.isEmpty()) { if (h > minH) { return flag.length; } List<Integer> list = map.get(queue.removeFirst()); for (int i : list) { if (!flag[i]) { queue.addLast(i); flag[i] = true; } } if (size == 0) { size = queue.size(); h++; } } return h; }
-
不過就算優化過了BFS求高度,依然還是超時
-
官方題解
- LeetCode美國站的官方題解給出了 O(N) 級別的演算法,不過這涉及到了圖的拓撲排序,所以這裡我們暫且擱置。
37. 解數獨(困難)
-
分析
- 數獨題,是十分經典的回溯演算法的題目。事實上,回溯法對於數獨題來說並不是最優解,不過我們這裡還是用了回溯法,作為對回溯法的鞏固
- 先遍歷整個列表,當我們遍歷到第
i
行第j
列的位置時- 如果這個位置是空白元素,那麼我們將儲存這個位置,方便後續的遞迴操作
- 如果這個位置是一個數字,那麼我們將
row[i][x - 1]
、column[j][x - 1]
、block[i / 3][j / 3][x - 1]
標記為true
。其中row[i][x - 1]
表示在第i
中,數字x
已經出現過了
- 然後再進行回溯搜尋
- 在遍歷到
board[i][j]
時, 嘗試填充數字x
, 其中要求row[i][x - 1]
、column[j][x - 1]
、block[i / 3][j / 3][x - 1]
均為false
,同時將上述三個位置標記為true
。
- 在遍歷到
- 重點:記得要讓遞迴及時停止
-
程式碼
public void solveSudoku(char[][] board) { boolean[][] row = new boolean[9][9]; boolean[][] column = new boolean[9][9]; boolean[][][] block = new boolean[3][3][9]; List<int[]> pos = new ArrayList<>(); for (int i = 0; i < 9; i++) { for (int j = 0; j < 9; j++) { if (board[i][j] != '.') { int p = board[i][j] - '1'; row[i][p] = true; column[j][p] = true; block[i / 3][j / 3][p] = true; } else { pos.add(new int[]{i, j}); } } } boolean[] flag = new boolean[1]; dfs(board, pos, 0, row, column, block, flag); } private void dfs(char[][] board, List<int[]> pos, int n, boolean[][] row, boolean[][] column, boolean[][][] block, boolean[] flag) { if (n == pos.size()) { flag[0] = true; return; } int[] o = pos.get(n); int r = o[0]; int c = o[1]; for (int i = 0; i < 9; i++) { if (!row[r][i] && !column[c][i] && !block[r / 3][c / 3][i] && !flag[0]) { row[r][i] = true; column[c][i] = true; block[r / 3][c / 3][i] = true; board[r][c] = (char) (i + '1'); dfs(board, pos, n + 1, row, column, block, flag); row[r][i] = false; column[c][i] = false; block[r / 3][c / 3][i] = false; } } }