代码随想录算法训练营第27天 | 39、40、131

文章讲述了使用回溯算法解决组合总和的问题,包括无重复元素的数组中找到所有加和为目标数的组合,以及考虑元素可重复使用的优化。重点在于回溯过程中的剪枝操作,确保结果不包含重复组合。此外,还介绍了处理有重复元素的组合总和问题,通过跳过相同元素避免重复组合。最后提到了分割回文串的问题,利用回溯法判断子串是否为回文并进行分割。

39.组合总和

题目描述

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例1:
输入:candidates=[2,3,6,7],target=7candidates = [2,3,6,7], target = 7candidates=[2,3,6,7],target=7
输出:[[2,2,3],[7]][[2,2,3],[7]][[2,2,3],[7]]
示例2:
输入:candidates=[2,3,5],target=8candidates = [2,3,5], target = 8candidates=[2,3,5],target=8
输出:[[2,2,2,2],[2,3,3],[3,5]][[2,2,2,2],[2,3,3],[3,5]][[2,2,2,2],[2,3,3],[3,5]]
示例3:
输入:candidates=[2],target=1candidates = [2], target = 1candidates=[2],target=1
输出:[][][]

思路

本题仍然是回溯组合的应用,仔细观察可以发现其中的不同:
1、candidates的元素不重复,但其中的数字可以无限使用,也就是说,在同一个组合中可以多次使用同一个元素,但返回的列表中不能存在相同的组合。
2、组合仍然不关注顺序,即1,2和2,1仍然为同一个组合。
那么如何表达元素在一个结果中可以无限使用呢,在每一次递归的时候不+1即可,也就是说,每次的递归选择中,无需从下一位数字选取,而是可以仍然选择当前的数字。
下面有需要考虑一个问题:递归的过程如何跳出
按照题目的要求,应该是和大于或等于target的时候即可跳出,其中如果相等需要记录该组合。
为了不将所有可能全部遍历,可以进行剪枝,那么如何剪枝呢?
可以看出,能从大于的情况进行剪枝的操作。当目前的和大于target之后这一支之后是不可能出现符合要求的组合的,需要注意的是,这样剪枝的前提是数组有序。故而需要先排序。

题解

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates);
        helper(res,new ArrayList<>(),candidates,target,0,0);
        return res;
    }
    public void helper(List<List<Integer>> res,List<Integer> path,int[] candidates,
    int target,int sum,int index){
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = index;i<candidates.length;i++){
            if(sum + candidates[i] > target) break;
            path.add(candidates[i]);
            helper(res,path,candidates,target,sum+candidates[i],i);
            path.remove(path.size()-1);
        }
    }
}

总结

本题主要有两个难点:
1、无限制重复选取在递归中应该如何表达
2、剪枝操作的使用

40.组合总和Ⅱ

题目描述

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例1:
输入:candidates=[10,1,2,7,6,1,5],target=8,candidates = [10,1,2,7,6,1,5], target = 8,candidates=[10,1,2,7,6,1,5],target=8,
输出:[[1,1,6],[1,2,5],[1,7],[2,6]][ [1,1,6], [1,2,5], [1,7], [2,6] ][[1,1,6],[1,2,5],[1,7],[2,6]]
示例2:
输入:candidates=[2,5,2,1,2],target=5,candidates = [2,5,2,1,2], target = 5,candidates=[2,5,2,1,2],target=5,
输出:[[1,2,2],[5]][ [1,2,2], [5] ][[1,2,2],[5]]

思路

本题的特色在于,有重复的元素,每个数字只能在每个组合中使用一次,同时仍然要求不能有重复的组合。
那么本题的重点自然也就是剪枝的操作,问题在于,剪哪里的枝
仔细思考可以发现,这些限制用常规的理解来表达就是,每一个数字用过了之后就不能再用了,但是两个相同的数字算作两个不同的个体。
那么能够快速想到的方法就是,对每个元素是否已经用在了此时的组合里进行记录,在使用一个元素时首先查询,若用过了就不能再用了。
而还有一种方法就是,跳过相同的元素,也就是,在递归循环的过程中,其实每个元素是都会被遍历一遍的,那么就可以通过在遍历的时候,直接使用startindex而避免使用之前已经使用过的元素。

题解1

class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> res = new ArrayList<>();
    boolean[] used;
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        used = new boolean[candidates.length];
        Arrays.fill(used,false);
        Arrays.sort(candidates);
        helper(candidates,target,0);
        return res;
    }
    public void helper(int[] candidates,int target,int startIndex){
        if(sum == target){
            res.add(new ArrayList(path));
        }
        for(int i = startIndex;i<candidates.length;i++){
            if(sum + candidates[i] > target){
                break;
            }
            if(i > 0 && candidates[i] == candidates[i-1] && !used[i-1]){
                continue;
            }
            used[i] = true;
            sum += candidates[i];
            path.add(candidates[i]);
            helper(candidates , target,i+1);
            used[i] = false;
            sum -= candidates[i];
            path.removeLast();
        }
    }
}

题解2

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        helper(candidates , target ,0);
        return res;
    }
    public void helper(int[] candidates,int target,int start){
        if(sum == target){
            res.add(new ArrayList<>(path));
            return ;
        }
        for(int i = start; i < candidates.length && sum + candidates[i] <= target;i++){
            if(i > start && candidates[i] == candidates[i-1]){
                continue;
            }
            sum += candidates[i];
            path.add(candidates[i]);
            helper(candidates , target , i+1);
            int tmp = path.getLast();
            sum -= tmp;
            path.removeLast();
        }
    }
}

总结

本题值得再多看几遍,这个剪枝操作的思考量真的很精彩

131.分割回文串

题目描述

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。

思路

本题涉及到了回溯的另一个应用方向——分割。在本题应该有两个部分的设计:
1、如何分割,即,分割的过程中,那一道分割线如何表达
2、是否回文
首先是如何分割,分割的回溯过程本质上和组合差不多,只不过每一层中切割的都是不同数量的元素从而形成树形结构。而分割线也可以使用我们熟悉的startindex来表示。
是否回文在本题中是在判断一个结果是否有被保存的资格,那么可以独立作为一个判断的方法,调用在递归中。

题解

class Solution {
    List<List<String>> res = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();
    public List<List<String>> partition(String s) {
        helper(s,0);
        return res;
    }
    public void helper(String s , int startIndex){
        if(startIndex >= s.length()){
            res.add(new ArrayList(path));
            return;
        }
        for(int i = startIndex;i< s.length();i++){
            if(isPalindrome(s,startIndex,i)){
                String string = s.substring(startIndex,i+1);
                path.addLast(string);
            }
            else{
                continue;
            }
            helper(s,i+1);
            path.removeLast();
        }
    }
    public boolean isPalindrome(String s, int startIndex, int end){
        for(int i = startIndex,j = end; i < j;i++,j--){
            if(s.charAt(i) != s.charAt(j)){
                return false;
            }
        }
        return true;
    }
}

总结

突然进入分割的题目还不太会,还是需要多看看多练练。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值