4道LeetCode一起学烂回溯算法

4道LeetCode一起学烂回溯算法

前言

回溯算法本身是一种穷举所有可能的暴力解法,只是它可以在穷举的过程通过剪枝操作避免一些不合理的情况,并且在回溯的过程中尽可能的保留已有的结果。回溯算法解决的是所有可能组合的问题。

  • 回溯函数框架
  1. 结束条件
    1.1. 满足结束条件,则将当前选择组合存入全局变量中(结果添加
    1.2. 结束本次递归(返回操作
  2. 遍历所有可选择情况(可选选择
    2.1. 判断当前所做的选择是否合法(剪枝操作
    2.2. 将当前选择加入路径(路径添加
    2.3. 递归回溯函数做下个元素的选择(下一节点
    2.4. 一种路径选择完成,逐步移除最后一个元素,空出位置放其它选择(路径回溯

labuladong的图

1. 全排列

  • 问题
/**
 * 全排列 46
 * 给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
 *
 * 示例 1:
 * 输入:nums = [1,2,3]
 * 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
 *
 * 示例 2:
 * 输入:nums = [0,1]
 * 输出:[[0,1],[1,0]]
 *
 * 示例 3:
 * 输入:nums = [1]
 * 输出:[[1]]
 *
 */
  • 思路
    以nums = [1,2,3]为例,我可以把它转换为这样一颗三叉树,并且值保留了符合条件的路径。
    在这里插入图片描述
  • 回溯函数
1. 结束条件:组合个数达到nums.length
2. 可选选择:遍历nums的所有元素,每个元素都是可能的选择
2.1 剪枝操作:如果路径中已经包含本次选择的元素,则继续尝试其它选择
2.2 路径添加:将当前选择的元素加入路劲
2.3 递归选择下一层节点
2.4 路径回溯 
  • 实现
 private static List<List<Integer>> res;
 public static List<List<Integer>> permute_0(int[] nums) {
        res=new ArrayList<>();//全局变量存储所有组合
        List<Integer> permutation=new ArrayList<>();
        backtrack(nums,permutation);
        return res;
    }

    /**
     * 将nums的当前组合permutation存储到res中。
     * @param nums
     * @param permutation
     */
    private static void backtrack(int[] nums,List<Integer> permutation)
    {
        //1. 结束条件:如果permutation的长度为nums数组的长度,代表一个组合完成,将当前组合加入res,并回溯
        if (permutation.size()==nums.length)
        {
            res.add(new ArrayList<>(permutation));
            return;
        }
        //2.可选选择: 遍历nums,获取当前值num
        for (int num : nums) {
            //2.1 剪枝操作:如果 permutation 中包含curr,则结束当前组合情况
            if (permutation.contains(num)) continue;
            //2.2 路径添加:num加入permutation
            permutation.add(num);
            //2.3 选择下一节点:递归下一层结点
            backtrack(nums,permutation);
            //2.4 路径回溯:回溯重置permutation的当前值
            permutation.remove(permutation.size()-1);
        }
    }

2. N皇后

  • 问题
/**
 * N皇后 51
 *
 * n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。(即皇后的上下左右,左上,左下,右上,右下都不能出现其它皇后)
 * 给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
 * 每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
 *
 * 示例1:
 * 输入:n = 4
 * 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
 * 解释:如上图所示,4 皇后问题存在两个不同的解法。
 *
 * 示例2:
 * 输入:n = 1
 * 输出:[["Q"]]
 *
 */
  • 思路
每一行只能放一个皇后,每一行的每一列都可能放皇后,那么可以回溯穷举所有可能。下图以4皇后为例

在这里插入图片描述

  • 回溯函数框架
1. 结束条件:如果一种组合road完成,则添加到结果res中
2. 做选择:循环选择每一个列作为当前行n皇后可能的位置
3. 剪枝操作:判断在当前位置放置皇后,是否与已有的皇后起冲突
4. 递归做下一行n皇后的位置
5. 回溯操作:为当前节点其它选择留出位置
  • 实现
 /**
     * 回溯算法
     *
     * 思路:
     * 1. 每一行只能放一个皇后,每一行的每一列都可能放皇后,那么可以回溯穷举所有可能
     *
     * @param n
     * @return
     */
    public static List<List<String>> solveNQueens(int n) {
        res=new ArrayList<>();
        List<String> road=new ArrayList<>();
        backtrack(n,road);
        return res;
    }

    //全局变量存储所有可能组合
    private static List<List<String>> res;
    /**
     * 尝试每一行可以放皇后的所有可能列,并将结果存入road
     * @param n
     * @param road
     */
    private static void backtrack(int n,List<String> road)
    {
        //1. 如果road.size()=n,说明已经找到一种放置n皇后的方案,将其存入res,并结束本次递归
        if (road.size()==n)
        {
            res.add(new ArrayList<>(road));
            return;
        }
        //2. 可选选择:遍历当前行所有可能放置皇后的位置
        for (int i = 0; i < n; i++) {
            //2.1 剪枝操作:如果皇后位置冲突,则探索其它可能
            int row = road.size();
            if(isConflict(road,row, i,n)) continue;
            //2.2 路径加入:将当前行放置n皇后的位置加入路径
            StringBuilder str= new StringBuilder();
            for (int j = 0; j < n; j++) {
                if (i==j)
                    str.append("Q");
                else str.append(".");
            }
            road.add(str.toString());
            //2.3 递归选择下一行:递归探索下一行可能放置的位置
            backtrack(n,road);
            //2.4 路径回溯
            road.remove(road.size()-1);
        }

    }
    /**
     * 检查将皇后放在(rowIndex,columnIndex)的位置是否会与其它皇后冲突
     *
     * 因为我们是从上往下一行一行添加皇后,此时选择的皇后位置的下边,左边,右边,左下,右下一定没有皇后,
     * 我们只需要检查上,左上,右上即可
     * @param road
     * @param rowIndex
     * @param columnIndex
     * @param n 皇后个数,即矩阵大小
     * @return
     */
    private static boolean isConflict(List<String> road,int rowIndex, int columnIndex,int n) {
        int rowNum = road.size();
        //1. 上
        for (int i = 0; i < rowNum; i++) {
            if (road.get(i).charAt(columnIndex)=='Q')
                return true;
        }
        //2. 左上
        for (int i=rowIndex-1,j=columnIndex-1; i>=0 && j>=0; i--,j--)
        {
            if (road.get(i).charAt(j)=='Q')
                return true;
        }
        //3. 右上
        for (int i=rowIndex-1,j=columnIndex+1; i>=0 && j<n; i--,j++)
        {
            if (road.get(i).charAt(j)=='Q')
                return true;
        }

        return false;
    }

3. 单词搜索

  • 问题
/**
 * 单词搜索 79
 *
 * 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。
 * 如果 word 存在于网格中,返回 true ;否则,返回 false 。
 *
 * 单词必须按照字母顺序,通过相邻的单元格内的字母构成,
 * 其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。
 * 同一个单元格内的字母不允许被重复使用。
 *
 * 示例1:
 * 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
 * 输出:true
 *
 * 提示:
 * m == board.length
 * n = board[i].length
 * 1 <= m, n <= 6
 * 1 <= word.length <= 15
 * board 和 word 仅由大小写英文字母组成
 *
 */
  • 递归实现
 /**
     * 回溯法
     *
     * 思路:
     * 1. 如果当前字母为word的第一个单词,则若以该位置的相邻位置可以找到剩下的所有单词,则返回true,否则返回false。
     * 2. 而该位置可以找到剩下的所有单词吗?这又是和1一样的问题,所以是递归过程
     * 3. 在递归中我们进行剪枝操作,就可以实现回溯。
     *
     * 时间复杂度:一个非常宽松的上界为 O(M*N*3^L),其中 M, N为网格的长度与宽度,L为字符串 word的长度。每次调用回溯函数时间复杂度为O(3^L)
     * 空间复杂度:O(M*N),visited是O(M*N) ,回调栈的深度最大为O(min(L,M*N));
     * @param board
     * @param word
     * @return
     */
    public static boolean exist(char[][] board, String word) {
        int m = board.length;
        int n = board[0].length;
        char[] chars = word.toCharArray();
        boolean[][] visited=new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                //所有可能开头的位置
                if (backtrace(i,j,0,board,chars,visited))
                    return true;
            }
        }
        return false;
    }

    /**
     * [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]
     * "ABCCED"
     * 表示从i,j的位置开始,能否从board中获得word中从第k个字母(从0开始计)到最后一个字母。visited保证字母不重复使用
     * @param i
     * @param j
     * @param k
     * @param board
     * @param word
     * @param visited
     * @return
     */
    private static boolean backtrace(int i,int j,int k,char[][] board,char[] word,boolean[][] visited)
    {
        //1. 如果当前位置不等于word[k],则返回false,否则如果k是word的最后一个位置说明获得了完成单词,返回true;
        if (board[i][j]!=word[k])
            return false;
        else if (k==word.length-1)
            return true;
        //2. 标记当前位置已经访问过
        visited[i][j]=true;
        //3. 剪枝操作:判断从i,j的相邻位置开始,是否能找到word中从k+1到word.length的所有字母,如果有一种情况存在,则返回true,否则返回false
        //3.1 上
        if (i-1>=0 && !visited[i - 1][j])
        {
            boolean b = backtrace(i - 1, j, k + 1, board, word, visited);
            if (b)
                return true;
        }
        //3.2 下
        if (i+1<board.length && !visited[i + 1][j])
        {
            boolean b = backtrace(i + 1, j, k + 1, board, word, visited);
            if (b)
                return true;
        }
        //3.3 左
        if (j-1>=0 && !visited[i][j-1])
        {
            boolean b = backtrace(i, j-1, k + 1, board, word, visited);
            if (b)
                return true;
        }
        //3.4 右
        if (j+1<board[0].length && !visited[i][j+1])
        {
            boolean b = backtrace(i, j+1, k + 1, board, word, visited);
            if (b)
                return true;
        }
        //4. 回溯
        visited[i][j]=false;

        return false;
    }
  • 标准回溯框架 实现
static boolean res=false;

    /**
     * 标准回溯法
     * @param board
     * @param word
     * @return
     */
    public static boolean exist_0(char[][] board, String word) {
        int m = board.length;
        int n = board[0].length;
        char[] chars = word.toCharArray();
        StringBuilder str_word = new StringBuilder();
        boolean[][] visited=new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j]==chars[0])
                {
                    str_word.append(chars[0]);
                    visited[i][j]=true;
                    backtrace_0(i,j,board,chars,str_word,visited);
                    if (res) return true;
                    else{
                        visited[i][j]=false;
                        str_word.deleteCharAt(0);
                    }
                }
            }
        }
        return false;
    }

    /**
     * 标准回溯算法写法
     * @param i
     * @param j
     * @param board
     * @param word
     * @param str_word
     * @param visited
     */
    private static void backtrace_0(int i,int j,char[][] board,char[] word,StringBuilder str_word,boolean[][] visited)
    {
        //1. 如果str_word的长度等于word的长度,则表明所有匹配的字符查找完毕,结束递归
        if (str_word.length()==word.length)
        {
            res=true;
            return;
        }
        //待匹配字符索引
        int k = str_word.length();
        //2. 获取下一个字母的所有可选情况(即上下左右四种情况)
        List<match> selects = new ArrayList<>();
        //上下左右
        if (i>0 && !visited[i-1][j])
        {
            selects.add(new match(board[i-1][j],new Integer[]{i-1,j}));
        }
        if (i<board.length-1 && !visited[i+1][j])
        {
            selects.add(new match(board[i+1][j],new Integer[]{i+1,j}));
        }
        if (j>0 && !visited[i][j-1])
        {
            selects.add(new match(board[i][j-1],new Integer[]{i,j-1}));
        }
        if (j<board[0].length-1 && !visited[i][j+1])
        {
            selects.add(new match(board[i][j+1],new Integer[]{i,j+1}));
        }
        //3. 遍历所有可选选择
        for (match select_ : selects) {
            char select=select_.getChr();
            //3.1 如果字母不匹配,则尝试其它选择
            if (select!=word[k]) continue;
            //3.2 将选择的字母加入str_word
            str_word.append(select);
            //3.3 visited标记当前位置已经匹配过
            Integer m = select_.getIndex()[0];
            Integer n = select_.getIndex()[1];
            visited[m][n]=true;
            //3.4 做下一选择
            backtrace_0(m,n,board,word,str_word,visited);
            //3.5 回溯
            visited[m][n]=false;
            str_word.deleteCharAt(str_word.length()-1);
        }
    }

    private static class match
    {
        public Character getChr() {
            return chr;
        }

        public void setChr(Character chr) {
            this.chr = chr;
        }

        public Integer[] getIndex() {
            return index;
        }

        public void setIndex(Integer[] index) {
            this.index = index;
        }

        public match(Character chr,Integer[] index) {
            this.chr = chr;
            this.index = index;
        }

        private  Character chr;
        private  Integer[] index;

    }

4. 子集

  • 问题
/**
 * 子集 78
 *
 * 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
 * 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
 *
 * 示例 1:
 * 输入:nums = [1,2,3]
 * 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
 *
 * 示例 2:
 * 输入:nums = [0]
 * 输出:[[],[0]]
 *
 */
  • 思路
1. 对于一个包含m个元素的数组,从中选择包含n个数字的不重复组合,每一个数字都m种选择,回溯选择即可。
2. 为了保证 不能 包含重复的子集,每次开始选择元素索引必须不能包含该元素之前的任何元素,
对于数组[1,2,3,4],当前选择元素是2,那么下一个元素一定不能选择1,2,只能选择后面的元素即3,4,
这样可以避免重复子集。
  • 实现
/**
     * 回溯法
     *
     * 设nums中有n个元素,即有n个位置待填充,而nums中每个数字对应于每个位置有两种选择,即要么填充,要么不填充,则最多有2^n个子集。
     * 如:       nums=[1,2,3]
     * 待填充的有3个位置:[][][],而nums的数字对于待填充的位置只有两种选择,要么填充,要么不填充,因此有2^3=6个子集
     *
     * 时间复杂度:O(n*2^n),子集长度有n种情况,每种长度子集的个数最多为2^n
     * 空间复杂度:O(n*2^n),
     * @param nums
     * @return
     */
    public static List<List<Integer>> subsets_0(int[] nums) {
        res=new ArrayList<>();
        //Arrays.sort(nums);
        for (int i = 0; i <= nums.length; i++) {
            backtrace(nums,new ArrayList<>(),i,0);
        }
        return res;
    }

    /**
     * 回溯获取nums中指定元素个数n的所有子集,并存入res。
     * curr_index用于避免重复子集
     * @param nums
     * @param subset
     * @param n
     */
    public static void backtrace(int[] nums,List<Integer> subset,int n,int curr_index)
    {
        //1. 如果子集subset中的元素个数达到n,则加入res
        if (subset.size()==n)
        {
            res.add(new ArrayList<>(subset));
            return;
        }
        //2. 探索nums中当前索引到最后一个索引的情况,当前索引依次递增,是为了避免重复子集
        for (int i = curr_index; i < nums.length; i++) {
            int num = nums[i];
            if (subset.contains(num))
                continue;
            subset.add(num);
            backtrace(nums,subset,n,++curr_index);
            subset.remove(subset.size()-1);
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mekeater

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值