回溯算法(BFS)

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

Ⅰ 解题套路

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

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

模板代码:

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

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

Ⅱ 相关习题

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

3

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

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

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

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> 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<List<String>> solveNQueens(int n) {
        //初始化棋盘
        char[][] map = new char[n][n];
        int row = map.length;
        int col = map[0].length;
        for (int i=0;i<row;i++) {
            for (int j=0;j<col;j++) {
                map[i][j] = '.';
            }
        }

        List<List<String>> res = new ArrayList<>();
        backtrack(map,0,res);
        return res;
        
    }


    //回溯函数!!!
    public void backtrack(char[][] map,int rowDepth,List<List<String>> res) {
        //结束条件
        if (rowDepth == map.length) {
            //arrToListStr(map) 转化棋盘的格式
            res.add(arrToListStr(map));
            return;
        }
        for (int i=0;i<map.length;i++) {
            if (judge(map,rowDepth,i)) {//如果放置的皇后不冲突
                map[rowDepth][i] = 'Q';              
                backtrack(map,rowDepth+1,res);
                map[rowDepth][i] = '.';
            }
        }

    }

    //判断皇后是否冲突 。row为当前放置皇后的行,col为列
    public boolean judge(char[][] map,int row,int col){
        //该皇后的同一列是否有皇后
        for (int i =0;i<row;i++) {
            if (map[i][col] == 'Q') {
                return false;
            }
        }
        //判断左上角(整条斜线)是否有皇后
        for (int i=row-1,j=col-1;i>=0&&j>=0;i--,j--) {
            if (map[i][j] == 'Q') {
                return false;
            }
        }
        //判断右上角(整条斜线)是否有皇后
        for (int i=row-1,j=col+1;i>=0&&j<map.length;i--,j++) {
            if (map[i][j] == 'Q') {
                return false;
            }
        }
        return true;

    }

    //棋盘是一个二维数组,要将其转化为每一行是字符串的List集合
    public List<String> arrToListStr(char[][] arr) {
        List<String> path = new ArrayList<>();
        for (int i=0;i<arr.length;i++) {
            path.add(new String(arr[i]));
        }
        return path;
    }
}

3、子集(LeetCode 78)

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

class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> path = new ArrayList<>();
        List<List<Integer>> 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<List<Integer>> res,List<Integer> path,int start) {
        
        res.add(new ArrayList<>(path));  //所有走过的路径都需要记录,因此没有结束判断
        for (int j=start;j<nums.length;j++) {
            path.add(nums[j]);
            helper(nums,res,path,j+1);
            path.remove(path.size()-1);
            }
        
    }
}

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

image-20201212200217242

4、组合(LeetCode 77)

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

class Solution {

    List<List<Integer>> res = new ArrayList<>();

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

    void banktrack (int n,int k,List<Integer> 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<String> generateParenthesis(int n) {

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

    }
    List<String> 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<Character> 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<String> generateParenthesis(int n) {
        if (n <= 0) {
            res.add("");
            return res;
        }
        backtrack(n,n,new StringBuilder());
        return res;
    }

    List<String> 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++) {
            for (int j=0;j<board[0].length;j++) {
                if (dfs(board,wordArr,i,j,0)) return true;
            }
        }
        return false;
    }

    boolean dfs(char[][] board, char[] word, int i, int j, int k) {
        //递归出口
        if(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]; 
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容