dfs-回溯

183 阅读3分钟

51. N-Queues

image.png n*n的棋盘,保证行、列、对角线不能其他棋子的情况下铺满棋盘。

Input: n = 4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]

image.png

我们可以把这个棋盘看成一颗树,每个行列对应的格子就是节点,根节点就是第一行的格子,其个数是棋盘的列数,这颗树的深度就是棋盘的行数。下一行的所有节点都是上一行节点的自节点, 比如t(1,1)有4个子节点t(2,1), t(2,2), t(2,3), t(2,4),t(1,2)的子节点同样是这四个。所以问题就转化为我们有1颗树,每个节点的子节点个数都是n(棋盘的列数),我们只需要对这棵树进行深度优先搜索找到满足条件的解即可。

graph TD;
  root --> t11["tree(1,1)"];
  root --> t12["tree(1,2)"];
  root --> t13["tree(1,3)"];
  root --> t14["tree(1,4)"];
  
  t12 -.-> t21["tree(2,1)"];
  t12 -.-> t22["tree(2,2)"];
  t12 -.-> t23["tree(2,3)"];
  t12 --> t24["tree(2,4)"];
  
  t24 --> t31["tree(3,1)"];
  t24 -.-> t32["tree(3,2)"];
  t24 -.-> t33["tree(3,3)"];
  t24 -.-> t34["tree(3,4)"];
  
  t31 -.-> t41["tree(4,1)"];
  t31 -.-> t42["tree(4,2)"];
  t31 --> t43["tree(4,3)"];
  t31 -.-> t44["tree(4,4)"];

class Solution {
    public List<List<String>> solveNQueens(int n) {
        if (n< 1) {
            return Collections.emptyList();
        }
        
        // 初始化棋盘
        char[][] matrix = new char[n][n];
        for (char[] rows : matrix) {
            for(int i = 0; i < n; i++) {
              rows[i] = '.';
            }
        }
        List<List<String>> result = new ArrayList<>();
        dfs(n, 0, matrix, result);
        return result;
    }    
    
    
    private void dfs(int n, int row, char[][] matrix, List<List<String>> result) {
        int maxCol = n;
        if (row == n) {
            List<String> list = new ArrayList<>();
            for(char[] chars : matrix){
                list.add(String.valueOf(chars));       
            }
            
            result.add(list);
            return ;
        }  
        // 每一行的每一列
        for (int col = 0; col < maxCol; col++) {
            // 尝试放queue,如果能放就进入下一行遍历
            if (isValid(matrix, row, col, maxCol)) {
                // 尝试放queue
                matrix[row][col] = 'Q';
                dfs(n, row + 1, matrix, result);
                // 尝试完毕,回滚以便下一列接着尝试
                matrix[row][col] = '.';     
            }
        }
    }
    
    private boolean isValid(char[][] matrix, int row, int col,int maxCol) {

        // 不需要再判断同一行,因为有回溯
        // 判断(row,col)对应的列是否有冲突的棋子
        for(int i = 0; i < row; i++) {
            if(matrix[i][col] == 'Q') {
                return false;
            }
        }
        // 判断(row,col)对应的上对角线是否有冲突的棋子
        for(int i = 1; i <= Math.min(row, col); i++) {
           // (row, col) = (2, 3)  row-2, col - 2 = (0, 1) 
            if(matrix[row-i][col-i] == 'Q') {
                return false;
            }
        }
        // 判断(row,col)对应的下对角线是否有冲突的棋子
        for (int i = 1; i <= Math.min(row, maxCol - col - 1); i++) {
            if (matrix[row-i][col+i] == 'Q') {
                return false;
            }
        }
        return true;
    }
}

46. Permutations

Given an array nums of distinct integers, return all the possible permutations. You can return the answer in any order.

Example 1:

Input: nums = [1,2,3] Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Constraints:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • All the integers of nums are unique.

这个问题就是 找出一堆数的全排列。

如果要解决这个问题,毫无疑问需要对这个问题进行抽象。可以发现,这个跟一棵树的节点组合是非常类似的,相当于数组中的数都是根节点的子节点,而子节点的子节点都是数组中的数(排除他自己)

graph TD;
		root --> 1[1];
		root --> 2[2];
		root --> 3[3];
    1 --> s11[1];
    1 --> s12[2];
    1 --> s13[3];
    2 --> s21[1];
    2 --> s22[2];
    2 --> s23[3];
    3 --> s31[1];
    3 --> s32[2];
    3 --> s33[3];
    s12 --> s111[1];
    s12 --> s112[2];
    s12 --> s113[3];

因此,我们获得全排列实际上是在遍历这棵树,因此思路跟上面N-queue是一样的。

因为题中的排列不需要重复,因此用isValid方法去校验即可。

 class Solution {
     public List<List<Integer>> permute(int[] nums) {
         List<Integer> arr = new LinkedList<>();
         List<List<Integer>> res = new LinkedList<> ();
         dfs(nums, 0, arr, res);
         return res;
     }
     
     private void dfs(int[] nums, int level, List<Integer> arr, List<List<Integer>> res)  {
         
         if(level == nums.length) {
             // arr是在整颗树的遍历中引用的,因此这里必须要重新copy一份arr
             res.add(new LinkedList<>(arr));
             return ;
         }
         
         for (int i = 0; i < nums.length; i++) {
             if(isValid(arr, nums[i])) {
                 arr.add(nums[i]);
                 dfs(nums, level + 1, arr, res);
                 arr.remove(arr.size() - 1);
             }
         }
     }
     // 判断当前的数字是否在数组中已有重复
     private boolean isValid(List<Integer> arr, int num) {
         for (Integer integer : arr) {
             if(integer.equals(num)) {
                 return false;
             }
         }
         return true;
     }
     
 }

79 Word Search

检查一个网格中是否有指定的字符串

image-20220207231823156
 Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ASF"
 Output: true

我们先简化问题,假设我们只需要以A为起点匹配ASF即可。以A为根节点,周围靠边的字母都是它的子节点,如果我们是找ASF的话,其实就是在这棵树中遍历,有匹配的就ok。但这道题如果是FCS也是true,说明网格中任意一个数字都可能作为起点,我们可以以每个数字作为跟节点做一次遍历。

graph TD;
	A-->B;
	A-->S;
	B-.->A1[A]
	B-->C;
	B-->F;
	S-.->A2[A];
	S-->F;
	S-->A3[A]

visited[i][j] 用来记住走过的路径,防止走回去。

class Solution {
    public boolean exist(char[][] board, String word) {
        
        StringBuilder res = new StringBuilder();
        int m = board.length;
        int n = board[m-1].length;
        // 走过的路径状态记录
        boolean[][] visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {     
            for(int j = 0; j < n; j++){
                if(dfs(board, word, i, j, 0, res, visited)) {
                    return true;
                }      
            }
        }
        return false;
        
    }
    private boolean dfs(char[][] board, String word, int row, int col, int level, StringBuilder res, boolean[][] visited) {
        if (level == word.length()) {
            return true;
        }
        if(row>=board.length || row<0 || col>=board[row].length || col<0 || board[row][col] != word.charAt(level) || visited[row][col]){
            return false;
        } 
        
        visited[row][col] = true;
        if(dfs(board, word, row - 1, col, level + 1, res, visited) || 
           dfs(board, word, row + 1, col, level + 1, res, visited) || 
           dfs(board, word, row, col - 1, level + 1, res, visited) || 
           dfs(board, word, row, col + 1, level + 1, res, visited)) {
            return true;
        }
        visited[row][col] = false;
        return false;
    }
}

77. Combinations

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

这道题也可以抽象成树,并用回溯的方法解决,但在我最开始的解法中,是想的靠后的数为根节点的时候也要将它之前的数作为子节点去搜索,但这种方法到n=20,k=16就超时了。实际情况是前面的数在遍历的时候一定会将后面的数考虑进去,后面的数不应当再包含前面的数,否则就会重复解。所以后面的数自然就不需要考虑前面的数了,那dfs的时候就需要把每一次的起点都提高。

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<Integer> list = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        dfs(n, k, 1, list, res);
        return res;
    }
    
    private void dfs(int n, int k, int next, List<Integer> list, List<List<Integer>> res) {
        if(list.size() == k) {
            res.add(new ArrayList<>(list));
            return ;
        }
        for(int j = next; j <= n; j++) {
            if(!list.contains(j)) {
                  list.add(j);
                dfs(n, k, j+1, list, res);
                list.remove(list.size()-1);
            }
              
        }
        
    }
}