LeetCode Day27 39&40&131

108 阅读5分钟
39. 组合总和

这题和前面做过的题类似,重点注意同一个 数字可以 无限制重复被选取无重复元素

  1. 首先确定回溯的出入参数,同样用一个result数组去存储最后的返回结果,与此同时path记录下回溯过程中走过的每一条路径的结果集。前面做到的题目用一个startIndex去记录遍历循环的次数。二刷这道题的时候我在想这个startIndex的真正意义是什么,因为它是用来控制循环起始位置的,所以组合回溯的时候,进入下一次回溯我们就会在原来循环的次数上+1。此外还需要sum变量来统计单一结果path里的总和,这个sum可用可不用,如果不用的话就一直用target减去数组内的值然后判断target==0就好了。最后,对于组合问题,什么时候需要startIndex呢? 如果是一个集合来求组合的话,就需要startIndex,例如:[77.组合 ],[216.组合总和III]。 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[17.电话号码的字母组合]
  2. 递归的终止条件:当sum>target的时候可以直接返回了,如果sum==target成立那么说明找到了一条path是符合要求的答案,可以直接添加到result结果集中。
  3. 单层搜索逻辑:这道题和[77.组合]、[216.组合总和III]的一个区别是:本题元素为可重复选取的。这意味着我们在回溯遍历时不用i+1了,表示可以重复读取当前的数。
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);
        combinationSumHelper(candidates, target, 0, 0);
        return res;
    }

    public void combinationSumHelper(int[] candidates, int target, int startIndex, int sum){
       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;

            sum += candidates[i];
            path.add(candidates[i]);
            combinationSumHelper(candidates, target, i, sum);
            sum -= candidates[i];
            path.removeLast();
        }
    }
}
40. 组合总和 II

这道题目和39. 组合总和如下区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而39.组合总和 是无重复元素的数组candidates

如果选择先用回溯把所有组合情况求出来,然后再用hashset去重的话很容易超时,所以只能在回溯的过程中就实现去重步骤。组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。这里同一树枝上使用过其实是指同一个数字可否被反复选择,所以这道题的去重指的是同一树层上是否被使用过。

  1. 确定函数出入参:和39题的出入参相同,这里还需要加一个bool型数组isused,用来记录同一树枝上的元素是否使用过。
  2. 递归终止条件:终止条件为 sum > target 和 sum == target
  3. 单层循环逻辑:因为去重的是同一树层里使用过的,那我们判断同一树层上元素(相同的元素)是否使用过的标准就是这个isused==false(因为回溯一轮结束后上一个元素会被回溯回false)。所以这里的判断条件就是candidates[i] == candidates[i - 1] 并且 isused[i - 1] == false,条件符合时就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] isused;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        isused = new boolean[candidates.length];
        Arrays.sort(candidates);
        for(int i = 0; i < candidates.length; i ++) isused[i] = false;
        backTracing(candidates, target, 0, 0);
        return res;
    }

    public void backTracing(int[] candidates, int target, int startIndex, int sum){
       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;
            // 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过
            if (i > 0 && candidates[i] == candidates[i - 1] && !isused[i - 1]) {
                continue;
            }
            sum += candidates[i];
            isused[i] = true;
            path.add(candidates[i]);
            backTracing(candidates, target, i + 1, sum);
            sum -= candidates[i];
            isused[i] = false;
            path.removeLast();
        }
    }
}
131. 分割回文串

这道题涉及到 1.切割字符串 2.判断是否回文串

切割问题其实类似组合问题,切割出第一个字符以后从剩下的字符中往后切割,其实就相当于从剩下的字符中组合出符合题意的字符串,所以同样可以抽象成树形结构。

  1. 递归传入的出入参:全局变量数组path存放切割后回文的子串,二维数组result存放结果集。还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
  2. 递归终止条件:切割线切到了字符串的尾部说明切割完成了,找到了对应的回文串。
  3. 单层逻辑:我们在切割过程中定义了起始位置startIndex,那么[startIndex, i]就是切割的区间,与此同时我们需要判断这个子串是不是回文,如果是回文,就记录到path集合中去。注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
class Solution {
    List<List<String>> res = new ArrayList<>();
    Deque<String> path = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backtraing(s, 0);
        return res;
    }

    public void backtraing(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 str = s.substring(startIndex, i + 1);
                path.add(str);
            } else{
                continue;
            }
            backtraing(s, i + 1);
            path.removeLast();
        }
    }

    private boolean isPalindrome(String s, int start, int end){
        for(int i = start, j = end; i < j; i ++, j --){
            if(s.charAt(i) != s.charAt(j)){
                return false;
            }
        }
        return true;
    }
}