回溯算法(BFS)

深度优先搜索/回溯算法(DFS)

Ⅰ 解题套路

回溯问题实际上就是一颗决策树的遍历过程,需要思考三个问题:

  1. 路径:也就是已经做出的选择
  2. 选择列表:也就是当前可以进行的选择
  3. 结束条件:也就是到达决策树底层无法进行选择从而退出的条件

模板代码:

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择
    //其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」

Ⅱ 相关习题

1、没有重复数字的全排列(LeetCode 46)

3

​ 我们定义的 backtrack 函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性,每当走到树的底层,其「路径」就是一个全排列。

5
List> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List> permute(int[] nums) {
    // 记录「路径」
    LinkedList track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList track) {
    // 触发结束条件,表明到底了
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }

    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}

2、N皇后(LeetCode 51)

​ 给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。


image-20201212193833824
class Solution {
    public List> solveNQueens(int n) {
        //初始化棋盘
        char[][] map = new char[n][n];
        int row = map.length;
        int col = map[0].length;
        for (int i=0;i> res = new ArrayList<>();
        backtrack(map,0,res);
        return res;
        
    }


    //回溯函数!!!
    public void backtrack(char[][] map,int rowDepth,List> res) {
        //结束条件
        if (rowDepth == map.length) {
            //arrToListStr(map) 转化棋盘的格式
            res.add(arrToListStr(map));
            return;
        }
        for (int i=0;i=0&&j>=0;i--,j--) {
            if (map[i][j] == 'Q') {
                return false;
            }
        }
        //判断右上角(整条斜线)是否有皇后
        for (int i=row-1,j=col+1;i>=0&&j arrToListStr(char[][] arr) {
        List path = new ArrayList<>();
        for (int i=0;i

3、子集(LeetCode 78)

​ 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。说明:解集不能包含重复的子集。

class Solution {

    public List> subsets(int[] nums) {
        List path = new ArrayList<>();
        List> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) return res;
        helper(nums,res,path,0);
        return res;
        
    }

    public void helper(int[] nums,List> res,List path,int start) {
        
        res.add(new ArrayList<>(path));  //所有走过的路径都需要记录,因此没有结束判断
        for (int j=start;j

可以看见,对 res 的更新是一个前序遍历,也就是说,res 就是树上的所有节点:

image-20201212200217242

4、组合(LeetCode 77)

​ 输入两个数字 n, k,算法输出 [1..n] 中 k 个数字的所有组合。(要注意剪枝,因为[1,2]和[2,1]是相同的,应该去重)

class Solution {

    List> res = new ArrayList<>();

    public List> combine(int n, int k) {
        if (n == 0 || k == 0 || k > n) return new ArrayList<>(0);
        List path = new ArrayList<>();
        banktrack(n,k,path,1);
        return res;
        
    }

    void banktrack (int n,int k,List path,int start) {

        if (path.size() == k) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i=start;i<=n;i++) {
            
            path.add(i);
            //注意是i+1不是start+1
            banktrack(n,k,path,i+1);
            path.remove(path.size()-1);
        }

    }
}

5、解数独(LeetCode 37)

​ 输入是一个9x9的棋盘,空白格子用点号字符 ‘.’表示,算法需要在原地修改棋盘,将空白格子填上数字,得到一个可行解。数独的要求:每行,每列以及每一个 3×3 的小方格都不能有相同的数字出现。
​ 解法:直接对每个格子进行穷举,1-9一个一个试。与之前的回溯不同的是得到一个答案就结束,不要求得出所有答案。

class Solution {
    public void solveSudoku(char[][] board) {
        backtrack(board,0,0);
    }

    boolean backtrack ( char[][] board ,int row,int col ) {

        int r = 9; int c = 9;
        if (col == c) { //到达最后一列
            return backtrack (board,row+1,0);
            
        }

        if (row == r) { //说明9*9的棋盘都填完了
            return true;
        }

        if (board[row][col] != '.') {
            return backtrack(board,row,col+1);
        }

        for (char ch = '1';ch <= '9';ch++) {
            if (!isValid(board,row,col,ch)) continue;
            board[row][col] = ch;
            if (backtrack(board,row,col+1)) return true;
            board[row][col] = '.';
        }
        //都试完了没有答案,返回false
        return false;
    }

    // 判断 board[i][j] 是否可以填入 n
    boolean isValid(char[][] board, int r, int c, char n) {
        for (int i = 0; i < 9; i++) {
            // 判断行是否存在重复
            if (board[r][i] == n) return false;
            // 判断列是否存在重复
            if (board[i][c] == n) return false;
            // 判断 3 x 3 方框是否存在重复
            if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
                return false;
        }
        return true;
    }
}

6、括号生成(LeetCode 22)

​ 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

​ 方法一:n对括号就有2*n个位置,对这些位置进行回溯遍历,再加上括号有效性的判断(用时13ms,效率低):

class Solution {
    public List generateParenthesis(int n) {

        if (n <= 0) {
            res.add("");
            return res;
        }
        StringBuilder sb = new StringBuilder();
        char[] opt = {'(',')'};
        backtrack(n,sb,opt);
        return res;

    }
    List res = new ArrayList<>();
    void backtrack (int n,StringBuilder sb,char[] opt) {
        if (sb.length() == n*2) {
            String path = sb.toString();
            if (isValid(path,n)) {
                res.add(path);
            }
            return;
        }

        for (int i=0;i<=1;i++) {
            sb.append(opt[i]);
            backtrack(n,sb,opt);
            sb.deleteCharAt(sb.length()-1);
        }
    }

    boolean isValid (String path,int n) {
        Deque stack = new ArrayDeque<>();
        for (char ch:path.toCharArray()) {
            if (ch == '(') {
                stack.addLast(ch);
            }else if (ch == ')') {
                if (stack.size() == 0) {
                    return false;
                }else {
                    stack.removeLast();
                }
            }
        }
        return stack.size()==0?true:false;
    }
}

方法二:将n对括号分成左括号(left)n个,右括号(right)n个。由于是从左往右遍历,一个合法的括号生成应该是左括号要大于等于右括号(用时1mm,效率高)。

class Solution {
    public List generateParenthesis(int n) {
        if (n <= 0) {
            res.add("");
            return res;
        }
        backtrack(n,n,new StringBuilder());
        return res;
    }

    List res = new ArrayList<>();
    void backtrack (int left,int right,StringBuilder sb) {
        //下面两个条件成立说明生成括号不合法
        if (left > right) return;
        if (left<0 || right<0) return;

        //下面成立说明生成括号合法
        if (left == 0 && right == 0) {
            res.add(sb.toString());
            return;
        }

        //可以不用for循环,只要列举完选择列表就行
        sb.append('(');
        backtrack(left-1,right,sb);
        sb.deleteCharAt(sb.length()-1);

        sb.append(')');
        backtrack(left,right-1,sb);
        sb.deleteCharAt(sb.length()-1);
    }
}

7、二叉树前序、中序、后序遍历非递归皆为深度优先

​ 二叉树的跳转连接

8、矩阵中的路径——寻找单词(剑指offer 12)

image-20201216225638764
class Solution {

    public boolean exist(char[][] board, String word) {
        if (word == null || word.length() == 0) return false;
        char[] wordArr = word.toCharArray();
        for (int i=0;i= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
        if(k == word.length - 1) return true;
        //注意用这个方法来减少引入isVisited数组
        board[i][j] = '#';
        boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) || 
                      dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
        board[i][j] = word[k];
        return res;
    }

}

9、目标和(用回溯法效率低,可以化为子集背包问题 LeetCode 494)

image-20201220203847478
class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        if (nums == null || nums.length == 0) return 0;
        dfs(nums,0,S);
        return res;
    }

    int res = 0;

    //rest是剩余的值,rest=0才满足要求
    void dfs (int[] nums,int depth,int rest) {
        if (depth == nums.length) {
            if (rest == 0) {
                res++;
            }
            return;
        }

        //选择+号
        rest -= nums[depth];
        dfs (nums,depth+1,rest);
        rest += nums[depth]; 

        //选择-号
        rest += nums[depth];
        dfs (nums,depth+1,rest);
        rest -= nums[depth]; 
    }
}

你可能感兴趣的:(回溯算法(BFS))