回溯算法2:组合总和、组合总和||、分割回文串、复原IP地址

7.组合总和

例题39:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例

1.题目与之前的组合总和有所区别,数字可以重复取,注意回溯中的i;
2.本题抽象为树形结构搜索过程如图所示:
搜索过程
3.Java中排序函数是Arrays.sort();

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
    Arrays.sort(candidates);//为什么排序之后就没有重复结果了?
    backtracking(candidates,target,0,0);
    return res;
    }

    public void backtracking(int[] candidates,int target,int sum,int startIndex){
        if(sum>target){
            return;
        }
    if(sum==target){
        res.add(new ArrayList<>(path));
        return;
    }
    for(int i=startIndex;i<candidates.length;i++){
       /* if(sum+candidates[i]>target){
            break;不让大于target的进入递归,如果去掉就在if那里判断
        }*/
        path.add(candidates[i]);
        sum+=candidates[i];
        backtracking(candidates,target,sum,i);//只能取当前i与之后的i,不会取到之前的i
        //backtracking(candidates,target,sum,startIndex);这个会遍历重复的结果,如2,2,3和2,3,2
        path.removeLast();
        sum-=candidates[i]; 
    }
    }
}

8.组合总和||

例题40:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。
示例

1.本题难点在于数组中有重复元素,却不能有重复的组合。如果把所有结果找出来,再用set或者map去重很可能会超时,所以要在搜索时就去重。
2.这里的去重指的到底是哪一种?在同树层上去重和在树枝上去重。要搞清楚因为数组中有重复元素,所以树枝递归中是允许有重复元素的。而不允许有相同组合,也就是说for循环中,不能有相同的元素开始。
去重
3.在同层上去重有两种方法,一种使用used数组判定,一种判断当前开始递归的i与上次递归的index是否相等,如果相等就跳过。

class Solution {
    List<List<Integer>> res=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    Arrays.sort(candidates);
    backtracking(candidates,target,0,0);
    return res;
    }

    public void backtracking(int[] candidates,int target,int sum,int index){
        if(sum==target){
            res.add(new ArrayList<>(path));
            return;
        }

        for(int i=index;i<candidates.length && sum+candidates[i]<=target;i++){
            if(i>index && candidates[i]==candidates[i-1] ){//为什么用i>index可以在同层去重
                continue;
            }
            path.add(candidates[i]);
            sum+=candidates[i];
            backtracking(candidates,target,sum,i+1);
            sum-=candidates[i];
            path.removeLast();
        }
    }
}

9.分割回文串

例题131:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。
示例
切割问题也可以抽象为树形结构的搜索过程,如下图所示:
切割

Java中获取字符串某个字符用charAt()函数;
Java中提取一个字符串某个子串,用substring(start,end);注意这里是左开右闭区间。
该题的难点:将切割问题抽象为树型结构的组合问题,搞清楚横向for循环是从哪里开始,纵向递归是从哪里分割。
如何模拟切割线?startIndex就是开始位置,i就是结束的切割位置,其中就是分割的子串。
在递归中如何切割子串?用的substring()这个函数。
如何判断回文字符串?普通做法就是双指针往中间夹,可以优化。

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

    public void backtracking(String s,int startIndex){
        if(startIndex>=s.length()){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex;i<s.length();i++){
            //从startIndex开始到i的子串
            if(isHw(s,startIndex,i)){
                String str=s.substring(startIndex,i+1);
                path.add(str);
            }
            else{
                continue;
            }
            backtracking(s,i+1);
            path.removeLast();

        }
    }

    public boolean isHw(String s,int start,int end){
        while(start<=end){
            if(s.charAt(start)!=s.charAt(end)){
            return false;
            }
            else{
                start++;
                end--;
            }
        }
        return true;
    }
}

10.复原IP地址

例题93:有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例

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

   public void backtracking(String s,int startIndex,int putDh){
       if(putDh==3){
           if(isR(s,startIndex,s.length()-1)){
               res.add(s);
           }
           return;
       }
       //这个题不能用开始位置作为终止条件,而是分割成4段,也就是说有3个逗号
       for(int i=startIndex;i<s.length();i++){
           if(isR(s,startIndex,i)){
              s=s.substring(0,i+1)+"."+s.substring(i+1);
              putDh++;
              backtracking(s,i+2,putDh);//注意这里是插入逗号之后的位置
              putDh--;
              s=s.substring(0,i+1)+s.substring(i+2);//删掉添加的逗号
           }
           else{
               break;//直接退出这次分割
           }
       }
   }

   public boolean isR(String s,int start,int end){
       if(start>end){
           return false;
       }
       if(s.charAt(start)=='0' && start!=end){
           return false;
       }
       int num=0;
       for(int i=start;i<=end;i++){
           if(s.charAt(i)>'9' || s.charAt(i)<'0'){
               return false;
           }
           //计算该字符串对应的数
           num=num*10+(s.charAt(i)-'0');
           if(num>255 || num<0){
               return false;
           }
       }
       return true;
   }
}

注意Java中的substring(start,end),也可以直接写开始substring(start),表示从这开始到末尾的子串。
添加逗号之后,递归之后去掉逗号的字符下标要注意是除掉逗号那个下标。
从右到左拿到一个数,计算整个数的大小的方法是在for循环中对每位数10*num+num;

11.子集问题

例题78:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例

这个题和之前的分割组合都不同,组合和分割是收集树的叶子节点,而子集问题是收集树的所有节点。
其实子集问题也可以看作组合问题,但是是无序的,子集{1,2}和{2,1}是一样的。既然无序,取过的元素不会重复,写回溯的时候,for循环从startIndex开始,而不是从0开始。
什么时候for循环是从0开始,排列问题就是从0开始,因为集合是有序的,集合{1,2}和{2,1}是两个集合。
子集问题的树型结构
从上图可以看出,子集问题在遍历的时候是把所有节点都记录下来。

class Solution{
    List<List<Integer>> res=new ArrayList<>();
    LinkedList<Integer> path=new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums){
    backtracking(nums,0);
    return res;
    }
    
    public void backtracking(int[] nums,int startIndex){
        if(startIndex<=nums.length){  //注意这里是<=,因为每次传path进来i+1了
            res.add(new ArrayList<>(path));
        }
        if(startIndex>nums.length){
            return;
        }
        for(int i=startIndex;i<nums.length;i++){
            path.add(nums[i]);
            backtracking(nums,i+1);
            path.removeLast();
        }
    }
}

这个题不需要任何剪枝,因为要保存所有的节点。是标准的模板
区分组合、分割、子集问题。组合和分割是求树的叶子节点,子集问题是求树的所有节点。

12.周末总结

1.回溯的组合总和|| 和第一周的组合问题不同。这题没有数量要求,可以无限重复,但是有总和的限制,所以间接也是个数的限制。

2.组合总和||| 依旧是元素重复,但解集不能有重复的组合。难点就在于去重。去重分为树枝去重树层去重

都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
去重
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过

3.分割回文串难点:

  • 切割问题类似于组合问题
  • 如何模拟切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文

如果想到了用求解组合问题的思路来解决 切割问题本题就成功一大半了,接下来就可以对着模板照葫芦画瓢。
分割回文串

4.复原IP地址:比分割回文串多了限制,如只能分成4段,直接更改字符串插入逗号。
复原IP地址

5.求子集问题,在树形结构中收集所有的节点
求子集问题
这个题可以作为子集问题的模板。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值