回溯算法总结


当遇到组合问题的时候,要想到回溯法。

17. 电话号码的字母组合

class Solution {
    List<String> res = new ArrayList<>();
    // 储存隐射关系:这里要用Character
    Map<Character,String> map = new HashMap<>();
    public List<String> letterCombinations(String digits) {
        /**
        分析:
        组合问题要想到回溯算法
        第一次思考:如何储存隐射关系??处理隐射关系肯定是想到哈希表,但是哈希表只能多对一隐射,现在,一个数字对应多个字母,按道理是不能隐射的。那么只能一个数字隐射一个字符串来处理了。
        上面思考结束后,根据下标去遍历字符串中的字母,如果使用stringbulider,那么就要回朔。如果是使用string,那么直接dfs
         */
        if(digits.equals("")){
            return res;
        }
        map.put('2',"abc");
        map.put('3',"def");
        map.put('4',"ghi");
        map.put('5',"jkl");
        map.put('6',"mno");
        map.put('7',"pqrs");
        map.put('8',"tuv");
        map.put('9',"wxyz");
        findCombinstion(digits,0,new StringBuilder());
        return res;
    }
    public void findCombinstion(String digits,int index,StringBuilder sb){
        if(index == digits.length()){
            // 下标到达终点,添加字符串
            res.add(sb.toString());
            return ;
        }
        // 取字符
        char c = digits.charAt(index);
        // 取字符对应的字符串
        char[] letter = map.get(c).toCharArray();
        for(char x : letter){
            // 遍历字符串 添加字符
            sb.append(x);
            // 递归
            findCombinstion(digits,index+1,sb);
            // 回朔
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

若使用string写法,String拼接的时候String每次都新建一个StringBuilder对象,再转为String,相当于递归方法里面每次都新建一个新的String对象吧,方法外面的String一直没变。StringBuilder一直都是一个对象所以要先append,再还原(delete)

class Solution {
    
    Map<Character,String> map = new HashMap<>();
     ArrayList<String> res = new ArrayList<>();

    public List<String> letterCombinations(String digits) {

        if(digits.equals(""))
            return res;

        map.put('2',"abc");
        map.put('3',"def");
        map.put('4',"ghi");
        map.put('5',"jkl");
        map.put('6',"mno");
        map.put('7',"pqrs");
        map.put('8',"tuv");
        map.put('9',"wxyz");

        // 使用string
        findCombination(digits, 0, "");
        return res;
    }

    private void findCombination(String digits, int index, String s){

        if(index == digits.length()){
            res.add(s);
            return;
        }

        Character c = digits.charAt(index);
        String letters = map.get(c);
        // String拼接的时候String每次都新建一个StringBuilder对象,再转为String,相当于递归方法里面每次都新建一个新的String对象吧,方法外面的String一直没变。StringBuilder一直都是一个对象所以要先append,再还原(delete)
        for(int i = 0 ; i < letters.length() ; i ++){
            findCombination(digits, index+1, s + letters.charAt(i));
        }

        return;
    }
}

22. 括号生成

class Solution {
    // https://leetcode-cn.com/problems/generate-parentheses/solution/shou-hua-tu-jie-gua-hao-sheng-cheng-hui-su-suan-fa/
    List<String> res = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        /**
        分析:
        按道理来说组合问题是需要进行回溯操作的,但是本题看不到回溯的影子,为什么呢?因为java中string的特殊性!可以看下第17题的String解法题解。
        总体思路是:左括号n个,右括号n个,那么如果有左括号,就优先拼接左括号,如果剩余右括号多余剩余左括号,那么可以拼接右括号。
        在使用String的时候,内部是使用StringBuilder类,实际上已经帮我们做了回溯哦了!
         */
        dfs(n, n, "");
        return res;
    }

    private void dfs(int left, int right, String curStr) {
        if (left == 0 && right == 0) { // 左右括号都不剩余了,递归终止
            res.add(curStr);
            return;
        }

        if (left > 0) { // 如果左括号还剩余的话,可以拼接左括号
            dfs(left - 1, right, curStr + "(");
        }
        if (right > left) { // 如果右括号剩余多于左括号剩余的话,可以拼接右括号
            dfs(left, right - 1, curStr + ")");
        }
    }

}

39. 组合总和

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        /**
        分析:
        组合问题一般都是需要回溯的,像这种target目标值问题,一般都是使用减法回溯模式
         */
        if(candidates == null || candidates.length == 0){
            return res;
        }
        dfs(candidates,target,0);
        return res;
    }
    public void dfs(int[] candidates, int target,int index){
        if(target == 0){
            // 这里一定要使用new 因为path是一个全局变量,一直在变化
            res.add(new ArrayList<>(path));
            return;
        }
        if(target < 0){
            return;
        }
        // 不断的遍历index,同时在遍历中去去递归探寻能不能找到target == 0 的数字组合出来
        for(int i = index; i < candidates.length; i++ ){
            path.add(candidates[i]);
            dfs(candidates,target - candidates[i],i);
            // 在探寻结束后,要回朔,删除最后一个节点
            path.remove(path.size() - 1);
        }
    }
}

40. 组合总和 II

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        /**
        套用上一题代码后,将index 改成了 i+1,那么如何去重呢?先给数组排序,然后在遍历中就去continue去重
        针对重复数字的组合数问题,个人觉得还是使用used数组去重比较好
         */
        if(candidates == null || candidates.length == 0){
            return res;
        }
        Arrays.sort(candidates);
        this.used = new boolean[candidates.length];
        dfs(candidates,target,0);
        return res;
    }
    public void dfs(int[] candidates,int target,int index){
        if(target == 0){
            res.add(new ArrayList<>(path));
            return;
        }
        if(target < 0){
            return;
        }
        for(int i = index; i < candidates.length; i++){
            // 去重操作
            // 首先 i 必须比index大,其次判断前后两个值是否一致
            // 实际上这里的 i > index等价于 i > 0 && !used[i-1] ( 同一树层找到了重复数字)
            // if( i > index && candidates[i] == candidates[i - 1]){
            //     continue;
            // }
            // 同一树支下 是深度递归
            // 同一树层中 会和同一树支下深度递归重复了,所以剪枝
            if( i > 0 && candidates[i] == candidates[i - 1] && !used[i-1]){
                continue;
            }
            path.add(candidates[i]);
            // 标记访问
            used[i] = true;
            dfs(candidates,target - candidates[i],i+1);
            path.remove(path.size() - 1);
            // 回溯后取消标记
            used[i] = false;
        }
    }
}

46. 全排列

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        // 很简单的回溯问题,心得体会:涉及重复问题要想办法进行剪枝去重
        if(nums == null || nums.length == 0){
            return res;
        }
        // 这里只传了nums参数
        dfs(nums);
        return res;
    }
    public void dfs(int[] nums){
        // 根据题意,添加到res应该是全排列
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        // 遍历从0开始,然后想办法去重
        for(int i = 0; i< nums.length; i++){
            // 进行剪枝去重
            if(path.contains(nums[i])){
                continue;
            }
            path.add(nums[i]);
            dfs(nums);
            path.remove(path.size() - 1);
        }
    }
}

47. 全排列 II

本题有点难理解,理解剪枝操作是重点,像这种一般先排序,后剪枝。难点就是上一层数字used回溯后对下一层数字的影响!

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    // 定义used数组,判定该数字是否被使用过了
    boolean[]used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums == null || nums.length == 0){
            return res;
        }
        this.used = new boolean[nums.length];
        // 排序
        Arrays.sort(nums);
        dfs(nums);
        return res;
    }
    public void dfs(int[]nums){
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0; i < nums.length; i++){
            // 如果该数字被使用过 跳过循环
            if(used[i]){
                continue;
            }
            // !used[i- 1]表示 上一轮的数字被撤销了
            // 上面那句话比较难理解,为什么就表示上一轮的数字被撤销了?按道理说used[i-1]应该是true,被访问过了,如果是fasle,那么必然说明这个数字已经被撤销了
            // 那么为什么 上一轮数字被撤销了,就跳过循环?举个例子:1” 1 2 和 1 1” 2,如果1”是上一轮被撤销的,那么就代表该数后面可以继续使用,但是实际上,上一层已经有该全排列,如:1” 1 2
            if( i > 0 && nums[i] == nums[ i - 1] && !used[i- 1]){
                continue;
            }
            path.add(nums[i]);
            // 标记该数字已经使用
            used[i] = true;
            dfs(nums);
            path.remove(path.size() - 1);
            // 回朔后 该数字又可以重新被使用
            used[i] = false;
        }
    }
}

77. 组合

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        dfs(n,k,1);
        return res;
    }
    public void dfs(int n,int k,int index){
        if(path.size() == k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i <= n; i++){
            // 这样会有重复数字出现
            // 使用dfs应该使用一个index参数,每次进行叠加,从而不会用重复参数
            // if(path.contains(i)) continue;
            path.add(i);
            // 这里使用的参数是 i + 1,每次都取后面的值(保证了不会重复取值)
            dfs(n,k,i+1);
            path.remove(path.size() - 1);

        }
    }
}

78. 子集

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path  = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        dfs(nums,0);
        return res;
    }
    public void dfs(int[] nums,int index){
        // 本题最奇葩的是没有递归出口 直接就是add操作
        res.add(new ArrayList<>(path));

        for(int i = index; i < nums.length; i++){
            path.add(nums[i]);
            // 参数是 i + 1,每次都向后添加一个
            dfs(nums,i+1);
            path.remove(path.size() - 1);
        }

    }
}

90. 子集 II

总结:组合问题的重复元素去重思路是去使用used数组去标记

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[]used;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 排序是为了去重
        Arrays.sort(nums);
        this.used = new boolean[nums.length];
        dfs(nums,0);
        return res;
    }
    public void dfs(int[] nums,int index){
        // 添加路径
        res.add(new ArrayList<>(path));
        // 遍历树的宽度,重点理解 树的循环遍历和递归遍历
        for(int i = index; i < nums.length; i++){
            // 对于同一树层 后面的数字重复了,那么就可以直接剪枝
            // 对于同一树支 后面的数字重复了 并不影响
            // 这里的 i > index 等价于 i > 0 && !used[i-1]
            // if( i > index && nums[i] == nums[i - 1]) continue;

            if( i > 0 && nums[i] == nums[i - 1] && !used[i-1]) continue;
            path.add(nums[i]);
            used[i] = true;
            // 移动下标
            dfs(nums,i+1);
            // 回溯
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }
}

93. 复原 IP 地址

这题好难,我现在还没有理解到位

class Solution {
    List<String> res = new ArrayList<>();
    List<String> path = new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {
        //这里就是对字符串的预处理,但是对于测试用例来说我觉得用处不大,毕竟不会蠢到用13位数字让你分割
        if(s.length()<4 || s.length()>12){
            return res;
        }
        //这里就是套用最经典的回溯模板了,相比于分割字符串只加入分割线一个参数以外,这里还需要添加额外的层数参数level
        //因为合法的IP地址只有四段,我们不能无限对其进行分割
        backtracking(s,0,0);
        return res;
    }

    // 判断分割出来的每一段字符串是否是合法的IP地址
    boolean isValidIp(String s){
        //判断其是否含有前导0
        if(s.charAt(0)=='0' && s.length()>1){
            return false;
        }
        //长度为4就直接舍弃,加上这一步是为了后面parseInt做准备,防止超过了Integer可以表示的整数范围
        if(s.length()>3){
            return false;
        }
        //将字符转为int判断是否大于255,因为题目明确说了只由数字组成,所以这里没有对非数字的字符进行判断
        if(Integer.parseInt(s)>255){
            return false;
        }
        return true;
    }

    void backtracking(String s,int splitIndex,int level){
        //递归终止条件,分割的四个字符串都是合法的IP地址
        if(level==4){
            //在代码的最后再利用join函数加上“.”,构造IP地址的表示形式
            res.add(String.join(".",path));
            return;
        }
        for(int i=splitIndex;i<s.length();i++){
            //每一次分割之后,对剩余字符长度是否合理进行判断,剪枝操作,优化运行速度
            if((s.length()-i-1) > 3*(3-level)){
                continue;
            }
            //如果分割的字符串不是合理的IP地址,跳过
            if(! isValidIp(s.substring(splitIndex,i+1))){
                continue;
            }
            //把合法的IP地址段加入path存储
            path.add(s.substring(splitIndex,i+1));
            //每次把分割线往后移一位,且段数level+1
            backtracking(s,i+1,level+1);
            //进行回溯操作
            path.remove(path.size()-1);
        }
    }
}

79. 单词搜索

参考视频:
https://www.bilibili.com/video/BV1zA411J7hY?from=search&seid=3466956450785063587&spm_id_from=333.337.0.0

class Solution {
    boolean[][] used;
    char[][]board;
    String word;
    int m;
    int n;

    public boolean exist(char[][] board, String word) {
        // 把所有信息设置为全局变量
        this.m = board.length;
        this.n = board[0].length;
        this.used = new boolean[m][n];
        this.board = board;
        this.word = word;

        // 网格中的每一个字母 都有可能是word的第一个字母
        for(int row = 0; row < m; row++){
            for(int col = 0; col < n; col++){
                // 发现网格中的字母  等于word的第一个字母,就开始按照规则搜索
                if(board[row][col] == word.charAt(0)){
                    // dfs传入参数为 下标参数和word的index下标
                    boolean res = dfs(row,col,0);
                    // res 为搜索结果
                    if(res) return true;
                }
            }
        }
        // 整个网格都没有搜索到 就返回false
        return false;
    }
    public boolean dfs(int row,int col,int index){
        // 递归出口
        if(index >= word.length()){
            // 下标超过了 word长度
            return true;
        }
        
        // 剪枝 
        if(row < 0 || row >= m || col < 0 || col >= n || used[row][col]){
            // 越界了就不必要搜索 已经被使用过了就不必要搜索
            return false;
        }
        // 传入的网格的字母和index位置的字母不一致
        if(board[row][col] != word.charAt(index)){
            return false;
        }

        // 设置该位置已经被访问了
        used[row][col] = true;
        // 搜索:上下左右,同时下标+1
        boolean res = dfs(row+1,col,index+1) || dfs(row-1,col,index+1) || dfs(row,col+1,index+1) || dfs(row,col-1,index+1);
        // 回溯后 该位置可以继续被使用
        used[row][col] = false;
        return res; 
    }
}

131. 分割回文串

本题每次都要判断是否回文,可以使用动态规划进行优化

class Solution {
    List<List<String>> res = new ArrayList<>();
    List<String> path = new ArrayList<>();
    public List<List<String>> partition(String s) {
        /**
        分析:
        这是一道典型的回溯树。横向循环遍历,纵向递归遍历。
        第一次写的时候,递归条件判断出来,但是for循环(横向扩展)和递归遍历的逻辑(纵向扩展)没有想出来,这个是要利用字符串的substring api方法
         */
        dfs(s,0);
        return res;
    }
    public void dfs(String s,int index){
        if( index == s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index; i  < s.length(); i++){
            // 定义横向的末尾指针,用于截取s
            int endIndex = i;
            // 截取的不是回文串,直接跳过  这里的区间是[index,endIndex]
            if( !check(s,index,endIndex)) continue;
            // 截取字符串,在截取之前就得先判断截取的字符串是不是回文串
            // 这里的区间是[index,endIndex+1)
            path.add(s.substring(index,endIndex+1));
            dfs(s,i+1);
            // 回溯
            path.remove(path.size() - 1);

        }
        
    }
    public boolean check(String s,int index,int endIndex){
        while(index < endIndex){
            if(s.charAt(index) != s.charAt(endIndex)) return false;
            index++;
            endIndex--;
        }
        return true;
    }
}

401. 二进制手表

class Solution {
    // 直接把二进制的问题转换为数组累加问题
    int[] hours = new int[]{1,2,4,8,0,0,0,0,0,0};
    int[] minutes = new int[]{0,0,0,0,1,2,4,8,16,32};
    List<String> res = new ArrayList<>();
    public List<String> readBinaryWatch(int turnedOn) {
       // 方法二:采用回溯法
       backTrack(turnedOn,0,0,0);
       return res;
    
    }
    // 回溯的参数:num(需要点亮的灯,初始为turnedOn),index(点亮的下标)
    // hour(小时数)minute(分钟数)
    public void backTrack(int num,int index,int hour,int minute){
        // 剪枝操作
        if(hour > 11  || minute > 59){
            return;
        }
        // 递归出口,当点亮到第0栈灯的时候,那么回溯所有数据
        if(num == 0){
        // 进行字符串拼接
            StringBuilder sb = new StringBuilder();
            sb.append(hour).append(':');
            if(minute < 10){
                sb.append('0');
            }
            sb.append(minute);
            res.add(sb.toString());
            //记得return终止掉 
            return;
        }
        // 这里是从下标开始递归遍历
        for(int i = index; i < 10; i++){
            backTrack(num - 1, i+1,hour+hours[i],minute+minutes[i]);
        }

        
    } 
        
}
 // 第一次接触回溯思想的题目(其实就是暴力遍历+剪枝)
        // 方法一:调用Integer.bitCount() ->计算数字转换为二进制为1的个数
        // 时钟限定在[0,11],分钟限定在[0,59],暴力循环
        //定义结果集
    //      List<String> res = new LinkedList<>();
    //     for(int i = 0; i < 12; i++){
    //         for(int j = 0; j < 60; j++){
    //             // 调用Integer.bitCount()
    //             if(Integer.bitCount(i)+Integer.bitCount(j)==turnedOn){
    //                 // 拼接、修改string ->StringBuilder()
    //                 StringBuilder sb = new StringBuilder();
    //                 // StringBuilder 遵循链式编程
    //                 sb.append(i).append(":");
    //                 // 若分钟数小于10,那么需要在前面拼接一个0
    //                 if(j < 10){
    //                     sb.append("0");
    //                 }
    //                 sb.append(j);
                   
    //                 res.add(sb.toString());
    //             }
    //         }
    //     }
    //     return res;
    // }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值