1. 程式人生 > 實用技巧 >LeetCode-05-BFS和DFS

LeetCode-05-BFS和DFS

第五講 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),其中,mnL分別為字元表的長、寬和word的長度。顯而易見的,我們需要進行一些剪枝。
  • 優化

    • 參考官方題解給出的解決,優化程式碼結構如下

      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),其中 MN 是地圖的長和寬
  • 程式碼

    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),其中 MN 是地圖的長和寬
  • 程式碼

    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;
            }
        }
    }