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),其中,
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; } } }