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 行皇后放置后增加的修改条件。

    • 举例

      image-20201214181517768
      • 如图所示,红色标记的是第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;
            }
        }
    }
    
posted @ 2020-12-21 16:18  PrimaBruceXu  阅读(76)  评论(0编辑  收藏  举报