代码随想录刷题-回溯算法day24-27

99 阅读11分钟

第七章 回溯算法part01

今日内容:

●  理论基础 

●  77. 组合   详细布置


理论基础

回溯是递归的副产物,只要有递归,就一定有回溯。所以回溯函数也是递归函数,递归函数也是回溯函数。

回溯法: 回溯法解决的问题都可以抽象为树形结构。 所有回溯法的问题都可以抽象为树形结构!!!

因为回溯法解决的都是在结合中递归查找子集,集合的大小构成了树的宽度;递归的深度构成了树的深度。

回溯看起来很高深,但是其本质上并不是什么高效的算法,其本质就是穷举。

这里本来应该写回溯法的三要素,但目前我个人并不认为其与递归的三要素有何区别:

这里在此重复一下递归三要素:递归函数返回值与形参、终止条件、每一轮递归过程中的操作。

如果非要说回溯三要素与上述三要素有何区别的话,那就是在每一轮递归的过程中回进行一步回溯操作。这个要具体情况具体分析。

回溯算法的模板

void backtracking(参数) {
        if (终止条件) {
            存放结果;
            return;
        }
    
        for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
            处理节点;
            backtracking(路径,选择列表); // 递归
            回溯,撤销处理结果
        }
    }

77. 组合

未进行剪枝:

 class Solution {
        //path用来记录组合之后的一组结果   result用来存放所有的结果
        List<Integer> path=new ArrayList<>();
        List<List<Integer>> result=new ArrayList<>();
        public List<List<Integer>> combine(int n, int k) {
            backtracking(n,k,1);
            return result;
    
    
        }
        //因为已经将path 以及 result设置为了全局变量 所以这里回溯算法并不需要返回东西
        public void backtracking(int n,int k,int startIndex){
            //也就是找到了一组满足条件的组合
            //将其加入到result中
            if(path.size()==k){
                result.add(new ArrayList(path));
                return ;
            }
            //经过了上面的if语句  一旦到了下面的for循环的话
            //这个时候path就一定还没有找到足够的数
            for(int i=startIndex;i<=n;i++){
                path.add(i);
                backtracking(n,k,i+1);
                path.removeLast();
            }
    
        }
    }

第七章 回溯算法part02

今日内容: 

●  216.组合总和III

●  17.电话号码的字母组合 详细布置


216.组合总和III

暂未实现任何剪枝的代码 :具体思想看代码注释


class Solution {
        List<List<Integer>> result=new ArrayList<>();
        List<Integer> path=new ArrayList<>();
        public List<List<Integer>> combinationSum3(int k, int n) {
            int sum=0;
            for(int i=1;i<=k;i++){
                sum+=i;
            }
            //这里就是题中示例3给出的反例  就是我当前能够得到的最小和 都大于了n  
            //那么根本不可能找到有效的组合
            if(sum>n) 
                return result;
            backtracking(k,n,1);
            return result;
        }
        //这里暂时不考虑剪枝
        public void backtracking(int k,int n,int startIndex){
            //回溯函数的终止条件 —— 找到了叶子节点(即path的size等于k 并且他们之和等于n)
            if(path.size()==k){
                int tempSum=0;
                for(int index=0;index<k;index++){
                    tempSum+=path.get(index);
                }
                if(tempSum==n){
                    //这里一定要额外调用ArrayList的构造函数 利用path重新创建一个ArrayList
                    result.add(new ArrayList(path));
                }
                return;
            }
            //当path中的数的个数还没有达到k时
            for(int i=startIndex;i<=9;i++){
                path.add(i);
                backtracking(k,n,i+1);
                //回溯
                path.removeLast();
            }
        }
    }

上面的代码可进行小小的优化——在每取到一个数时候,就直接计算其sum

既可以将sum放在全局变量中,也可以将其传递进入回溯函数。

 class Solution {
        List<List<Integer>> result=new ArrayList<>();
        List<Integer> path=new ArrayList<>();
        public List<List<Integer>> combinationSum3(int k, int n) {
            backtracking(k,n,1,0);
            return result;
        }
        public void backtracking(int k,int n,int startIndex,int curSum){
            if(path.size()==k){
                if(curSum==n)
                    result.add(new ArrayList(path));
                return;
            }
            for(int i=startIndex;i<=9;i++){
                //每次将一个新的数放入path之后  curSum也要对应地加上这个数
                path.add(i);
                curSum+=i;
                backtracking(k,n,i+1,curSum);
                //回溯同理  curSum也要对应地减去这个数
                path.removeLast();
                curSum-=i;
            }
        }
    }

17.电话号码的字母组合

这道题目:电话号码的字母组合。是涉及到不同集合之间的组合。

这里我一开始就想错了,知道是使用回溯,但是仍然纠结于用两层for循环(期望是两层for循环+递归来解答该题)。 个人谨记 : 一旦要使用了递归函数,其目的就是为了省略很多层的for循环。


class Solution {
        //打表格
        Map<Character,String> map=new HashMap<>();
    
        //result用来记录最终结果
        List<String> result=new ArrayList<>();
        //StringBuilder path是用来在digits中给了多个“数字”时 在回溯函数中用来记录组合的
        StringBuilder path=new StringBuilder();
    
        public List<String> letterCombinations(String digits) {
            //打表格
            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");
            //如果digits里面什么都没有
            if(digits.equals("")){
                return new ArrayList<>();
            }   
            //如果digits只给了一个“数字”的话 
            if(digits.length()==1){
    
                String str=map.get(digits.charAt(0));
                for(int i=0;i<str.length();i++){
                    StringBuilder strBild=new StringBuilder();
                    strBild.append(str.charAt(i));
                    result.add(strBild.toString());
                }
                return result;
            }
            //接下来就剩下digits中多个“数字”的情况 要处理
            backtracking(digits,0);
            return result;
        }
        //
        public void backtracking(String digits,int num){
            //如果递归到了叶子节点 并且path的length等于digits.length ()
            //这时候就说明我们已经找到了一个组合  
            if(path.length()==digits.length()){
                //因为我们这里设定path是StringBuilder对象类型  要调用其toString()方法完成转换
                result.add(path.toString());
                return;
            }
            //获取当前“数字”所对应的字符串 如'2'对应"abc"
            String s=map.get(digits.charAt(num));
            for(int i=0;i<s.length();i++){
                path.append(s.charAt(i));
                backtracking(digits,num+1);
                path.deleteCharAt(path.length()-1);
            }
        }
    }

第七章 回溯算法part03

●  39. 组合总和

●  40.组合总和II

●  131.分割回文串 详细布置


39. 组合总和

本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制

具体代码: 我感觉应该是考虑到了剪枝 就是判断curSum>target那里

个人心得记录:没想到这道理在没有看任何解析的情况下做出来了。个人总结主要原因如下:

  • 我知晓了什么情况下使用递归OR回溯

    • 当题目本身的数据结构就是有关树或者二叉树的,就要尝试使用递归

    • 当题目的相关问题能够转换成一棵树的时候,就要尝试递归

    • 不要忘了递归OR回溯三要素,


class Solution {
    List<List<Integer>> result;
    List<Integer> path;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //初始化
        result=new ArrayList<>();
        path=new ArrayList<>();
        backtracking(candidates,target,0,0);
        //因为result是全局的  backtracking调用完之后 result就更新了
        return result;
    }
    //考虑使用回溯算法   
    //特别注意这里元素是可以无限制地重复选用的
    public void backtracking(int[] candidates,int target,int curSum,int startIndex){
        //终止条件:
        //当前的和 大于了 目标和,那么return
        if(curSum>target){
            return;
        }
        //如果当前的和等于了目标和,那么就要存入result 当然也要return
        if(curSum==target){
            result.add(new ArrayList(path));
            return;
        }
        for(int i=startIndex;i<candidates.length;i++){
            //将该问题转换为一棵树  先处理当前遇到的节点
            //先把当前节点加入到path中  curSum也要更新
            curSum+=candidates[i];
            path.add(candidates[i]);
            //递归
            //因为元素是可以重复使用的 所以还是从当前的i开始
            backtracking(candidates,target,curSum,i);
            //回溯
            curSum-=candidates[i];
            //path移除最后一个元素
            path.remove(path.size()-1);
        }
    }
}

40.组合总和II

本题开始涉及到一个问题了:去重。

注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。 

    class Solution {
        //总体思路感觉应该跟39组合总数一样。 但是这里解集不能包含重复的组合。
        List<Integer> path;
        List<List<Integer>> result;
        boolean[] used;
        public List<List<Integer>> combinationSum2(int[] candidates, int target) {
            //这里尝试先排序  利用排序后的candidates再来组合  以此来尝试避免重复
            Arrays.sort(candidates);
            //初始化
            used=new boolean[candidates.length];
            Arrays.fill(used,false);
            result=new ArrayList<>();
            path=new ArrayList<>();
            backtracking(candidates,target,0,0);
            return result;
        }
        public void backtracking(int[] candidates,int target,int curSum,int startIndex){
            if(curSum>target){
                return;
            }
            if(curSum==target){
                result.add(new ArrayList(path));
                return;
            }

            for(int i=startIndex;i<candidates.length;i++){
               //排除可能重复出现的组合
               //因为我的candidates是排过序之后的 
               //所以唯一可能出现重复组合的情况就是  树的同一层中出现重复的数(而不是同一个树枝中)
                if( i>0 && candidates[i]==candidates[i-1] && used[i-1]==false){
                    //开启下一轮
                    continue;
                }
                //遇到节点 先处理当前节点
                curSum+=candidates[i];
                path.add(candidates[i]);
                used[i]=true;
                //递归处理下一个节点
                backtracking(candidates,target,curSum,i+1);
                //回溯
                curSum-=candidates[i];
                path.remove(path.size()-1);
                used[i]=false;
            }
        }
    }


131.分割回文串

135e8142-f06e-41aa-9f5d-e6b04c291a1b.png


class Solution {
    List<List<String>> result;
    List<String> path;
    public List<List<String>> partition(String s) {
        result=new ArrayList<>();
        path=new ArrayList<>();
        backtracking(s,0);
        return result;

    }
    //在分割问题中 startIndex可以类比成切割的那条线
    public void backtracking(String s,int startIndex){
        if(startIndex>=s.length()){
            result.add(new ArrayList(path));
            return;
        }
        for(int i=startIndex;i<s.length();i++){
            //如果是回文子串 那么就进行记录 
            if(isPalindromicString(s,startIndex,i)){
                String str=s.substring(startIndex,i+1);
                path.add(str);
            }
            else
                continue;
            backtracking(s,i+1);
            path.remove(path.size()-1);
            
        }

    }

    //判断是否是回文子串
    public boolean isPalindromicString(String s,int startIndex,int end){
        //使用双指针来遍历
        int left=startIndex;
        int right=end;
        while(left<=right){
            if(s.charAt(left)!=s.charAt(right)){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }

}

28 第七章 回溯算法

●  93.复原IP地址 

●  78.子集 

●  90.子集II   详细布置


93.复原IP地址

真没想到 这道题我竟然能做出来。

个人记录: 因为时间原因,下次有时间看卡哥的讲解以及代码的优化问题

class Solution {
        List<String> result;
        List<String> path;
        public List<String> restoreIpAddresses(String s) {
            //  初始化
            //result不用多说 就是用来存储返回的结果的。 
            //path是用来存储被切的每一小段 最后将path中存储的每一小段合在一起  就只会是result中的一个结果
            result=new ArrayList<>();
            path=new ArrayList<>();
            backtracking(s,0,0);
            return result;
        }
        public void backtracking(String s,int stratIndex,int IPlength){
            //return的条件
            //如果path被切成了四段  那么就是到了叶子结点
            if(path.size()==4){
                //如果是切割出来的是符合要求的IP地址
                //将其连接在一起 并且存入到result之中
                if(isIP(path) && IPlength==s.length()){
                    //将path中的每一小段合并在一起  然后存入到result中  记得加上点.
                    StringBuilder sBuilder=new StringBuilder();
                    for(int index=0;index<path.size()-1;index++){
                        sBuilder.append(path.get(index));
                        sBuilder.append('.');
                    }
                    sBuilder.append(path.get(path.size()-1));
                    result.add(new String(sBuilder.toString()));
                }
                return;
            }
            for(int i=stratIndex;i<s.length();i++){
                //String中的substring方法是包前不包后
                //这里尝试不做任何判断
                String str=new String();
                //因为所谓切除来的IP地址(暂不考虑IP地址是否正确) 它要满足两个基本要求
                //1、必须要切四段  2、必须要将整个s切完
                if(path.size()<4)
                    str=s.substring(stratIndex,i+1);
                else
                    str=s.substring(stratIndex,s.length());
    
                path.add(str);
                IPlength+=str.length();
                backtracking(s,i+1,IPlength);
                //这里要先处理IPlength 再去path.remove
                IPlength-=path.get(path.size()-1).length();
                path.remove(path.size()-1);
    
            }
    
    
    
        }
        //判断是否是符合要求的IP地址
        //这里考虑的是这个IP地址已经被完完整整切完了
        public boolean isIP(List<String> list){
            if(list.size()!=4)
                return false;
            for(int i=0;i<list.size();i++){
                //先判断不能是 前导0
                String str=list.get(i);
                if(str.length()>1 && str.charAt(0)=='0')
                    return false;
                //每个整数必须位于 0 到255之间
                //因为如果直接Integet.parseInt(str)的话 他可能会溢出 比如"5525511135" 所以首先只需要简单判断一下 
                //只要str的长度>=4了 那么它肯定就超过了255了   其长度<4的话  还有后面的那个条件 所以可以完成判断
                if(str.length()>=4 || Integer.parseInt(str) - 255 >0)
                    return false;
            }
            return true;
        }
    }


78.子集

这道题我个人感觉比之前的都重要一些

它考察的点反而更加倾向于递归or回溯的三要素:

  • 递归函数返回值以及形参是什么

  • 递归函数什么时候return

  • 每轮递归的时候具体做什么

这道题考察的重点在于递归函数什么时候return。 一开始我也是在头脑风暴,空想半天想不出来,但是一动笔在纸上画一画这棵树 ,一下子就明了了。

好记性不如烂笔头,古人诚不欺我。

class Solution {
        //nums中给了2个数字 就要2个for循环 给了5个数字就要5个for循环  
        //在出现n个for循环 且完全不知道n是多少的情况下  就可以使用递归&回溯方法
        //感觉这道题就是组合问题
        //空集就单独一开始直接加入即可  暂不考虑放入递归中
        List<Integer>path;
        List<List<Integer>> result;
        public List<List<Integer>> subsets(int[] nums) {
            path=new ArrayList<>();
            result=new ArrayList<>();
            result.add(new ArrayList());
            backtracking(nums,0);
            return result;
        }
        //因为这道题奇怪的地方在于  它并不是很好判断什么时候到了叶子节点 
        //它并不像找到复核某个targetSum的组合问题
        public void backtracking(int[]nums,int startIndex){
            //比如根据题目中的示例   我们可以发现{1,2,3} 每次递归到3 就可以终止  然后return了
            if(startIndex==nums.length)
                return;
            for(int i=startIndex;i<nums.length;i++){
    
                path.add(nums[i])
                //本题中应该不会出现“反例” 碰到就直接加入即可
                result.add(new ArrayList(path));
                backtracking(nums,i+1);
                path.remove(path.size()-1);
            }
        }
    
    }
    

90.子集II

这道题是在78子集 题目的基础上 新增了一个nums中可能包含重复元素的情况。

也就是说要进行去重的操作。因为一旦有重复的元素 就完全有可能出现重复的子集

class Solution {
        List<Integer> path;
        List<List<Integer>> result;
        boolean[] used;
        public List<List<Integer>> subsetsWithDup(int[] nums) {
            path=new ArrayList<>();
            result=new ArrayList<>();
            //
            used=new boolean[nums.length];
            Arrays.fill(used,false);
            //在nums经过排序的情况下, 如果nums[i-1]在此前已经取到集合了  
            //那么与nums[i-1]同值的nums[i]就没有任何用了 因为他们必定取到的是相同的集合
            Arrays.sort(nums);
            result.add(new ArrayList<>());
            backtracking(nums,0);
            return result;
        }
        public void backtracking(int[] nums,int startIndex){
    
            if(startIndex==nums.length)
                return;
            for(int i=startIndex;i<nums.length;i++){
                 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
                    continue;
                }
                path.add(nums[i]);
                result.add(new ArrayList(path));
                used[i] = true;
                backtracking(nums, i + 1);
                used[i] = false;
                path.remove(path.size()-1);
    
            }
        }
    
    }