代码随想录算法训练营day30 | 39. 组合总和 40.组合总和II 131.分割回文串

135 阅读4分钟

39. 组合总和

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

39. 组合总和 - 力扣(Leetcode)

思路

这道题给定的数组中元素是不重复的,但每个元素可以不限次数拿取,因此本题的难点在于如何去重。
去重的问题问题是在回溯过程中取值时产生的,如当 target = 7时,取值顺序可以是2 -> 2 -> 3,也可能是2 -> 3 -> 23 -> 2 -> 2 ,在取值时没有约束,导致最终结果相同。
为了解决这个问题,我们可以首先将给定数组 candidates 进行排序(如自然排序),然后在取值时只允许从同一个方向进行取值,即如果当前取到值的下标为startIndex,那么下一次的取值范围为[startIndex, candidates.length - 1],这样使得取值结果中所有的元素都是不递减的方式取得的,因此只会取一次,避免了重复。
当当前取值的所有元素的和total与将要取值的值的和大于target时,不用继续取值,可以进行剪枝。

代码

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> combination = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();

        Arrays.sort(candidates);

        backTracking(candidates, target, combination, 0, 0,res);

        return res;
    }

    private void backTracking(int[] candidates, int target,List<Integer> combination, int total, int startIndex, List<List<Integer>> res){

        if(total == target){
            res.add(new ArrayList<Integer>(combination));
            return;
        }

        for(int i = startIndex; i < candidates.length && (total + candidates[i]) <= target; i++){
            combination.add(candidates[i]);
            backTracking(candidates, target, combination, total + candidates[i], i, res);
            combination.remove(combination.size() - 1);
        }

    }
}

40.组合总和II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。

40. 组合总和 II - 力扣(Leetcode)

思路

39.组合总和 相比,本题的 candidates中的元素是可重复的,并且每个元素只能使用 1 次。
由于元素是可重复的,在回溯树中,对于同一层的两个相同元素,之后的遍历路径是相同的,结果会存在重复。因此,在某一层中,与当前元素相同的元素已经进行遍历时,当前元素不进行遍历,可以达到去重的效果。
为了方便判断相同元素已经进行过遍历,将 candidates先行进行排序,这样,相同的元素将会相邻排布,只需判断当前元素与前一元素是否相同即可。

代码

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {

        List<List<Integer>> res = new ArrayList<>();
        List<Integer> combination = new ArrayList<>();

        Arrays.sort(candidates);

        backTracking(candidates, target, res, combination, 0, 0);

        return res;
    }

    private void backTracking(int[] candidates, int target, List<List<Integer>> res, List<Integer> combination, int total, int startIndex){

        if(total == target){
            res.add(new ArrayList<>(combination));
            return;
        }

        for(int i = startIndex; i < candidates.length && total + candidates[i] <= target; i++){

            // 同一层中相同元素后续的遍历结果相同,此判断为去重操作
            if(i > startIndex && candidates[i - 1] == candidates[i]){
                continue;
            }

            combination.add(candidates[i]);
            backTracking(candidates, target, res, combination, total + candidates[i], i + 1);
            combination.remove(combination.size() - 1);
        }
    }
}

131.分割回文串

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

131. 分割回文串 - 力扣(Leetcode)

思路

这道题主要问题有2个:

  1. 如何判断回文?
  2. 如何分隔字符串?

判断回文的方法 : 双指针法

  • 对于字符串 s, 使用ij作为两个指针分别指向字符串的两个字符,每次判断s[i, j] 是否是回文。
  • 由上可知,i <= j
  • 对于字符串s[i, j],有以下三种情况:
    • i == j => s[i] == s[j],字符串是回文;
    • i + 1 == j => s[i]s[j] 相邻,字符串长度为 2
      • s[i] == s[j] => 字符串是回文
      • 否则, 字符串不是回文。
    • 否则,s[i]s[j] 之间还有其他字符,这两个字符为字符串的两端。当s[i] == s[j] 并且 二者之间的字符串 s[i + 1, j - 1]也是回文时, s[i, j] 是回文;否则,它不是回文。
  • 由上述描述可知,要知道字符串s[i, j]是不是回文,需要知道字符串s[i + 1, j - 1]是不是回文。
  • 也就是说,当i在遍历时,要先遍历i + 1再遍历i反方向),当遍历 j 时,要先遍历j - 1再遍历j正方向)。
  • 因此,i遍历的下标是从s.length() - 10
  • 由于i <= j, 要判断的字符串范围为 s[i, i]s[i, s.length() - 1]
  • 因此,相应地,对于确定的每一个i来说,j遍历的下标是从is.length() - 1

分割字符串:回溯法

  • 对于字符串s,使用splitIndex来标识下一刀开始分割的位置,即此字符串中,下标为[0, splitIndex - 1]的子串已经分割完毕,待分割的字符串下标范围为[splitIndex, s.length() - 1]
  • 初始条件中,待分割的字符串下标范围为[0, s.length() - 1],即 splitIndex0
  • 在每一次分割中,用i标识分割结束的下标,切出来的子串下标范围为 [splitIndex, i]左闭右闭区间);
    • 如果该子串是回文,则加入到当前结果集中,并递归分割剩余字符串[i + 1, s.length() -1],即下一次分割的 splitIndexi + 1;递归结束,不要忘记回退,不然会影响之后的递归结果。
    • 否则,这种分割方法达不到目标,不必继续分割剩余字符串(剪枝)。
  • splitIndex == s.length()时,整个字符串都分割完成,这意味着之前的每一次分割得到的字串都是回文,将当前结束集加入到最终结果集中即可以。

代码

class Solution {
    public List<List<String>> partition(String s) {
        boolean[][] isPalindrome = new boolean[s.length()][s.length()];

        List<List<String>> res = new ArrayList<>();
        List<String> path = new ArrayList<>();

        getPalindrome(isPalindrome, s);

        backTracking(s, isPalindrome, 0, res, path);

        return res;

    }

    private void getPalindrome(boolean[][] isPalindrome, String s){

        for(int i = s.length() - 1; i >= 0; i--){

            for(int j = i; j < s.length(); j++){

                if(i == j){
                    // s[i] == s[j]
                    isPalindrome[i][j] = true;
                    continue;
                }

                if(j - i == 1){
                    // s[i] 与 s[j] 相邻
                    isPalindrome[i][j] = s.charAt(i) == s.charAt(j) ? true : false;
                    continue;
                }

                if(s.charAt(i) == s.charAt(j) && isPalindrome[i+1][j-1]){
                    isPalindrome[i][j] = true;
                }else{
                    isPalindrome[i][j] = false;
                }
            }
        }
    }

    private void backTracking(String s, boolean[][] isPalindrome, int splitIndex, List<List<String>> res, List<String> path){

        if(s.length() == splitIndex){
            res.add(new ArrayList<String>(path));
            return;
        }

        for(int i = splitIndex; i < s.length(); i++){
            // 只有s[splitIndex, i]是回文的时候,才需要继续分隔字符串
            // tips :左闭右闭 区间
            // 否则,不符合要求,可以剪枝
            if(isPalindrome[splitIndex][i]){
                // 将字符串s[splitIndex, i] 加入 path
                path.add(new String(s.subSequence(splitIndex, i + 1).toString()));

                // 分隔剩余部分 s[i + 1,s.length() - 1]
                backTracking(s, isPalindrome, i + 1, res, path);

                // 回退
                path.remove(path.size() - 1);
            }
        }

    }
}

如果想让i正方向地进行遍历,也是可以的,此时,j >= i,子字符串为s[j, i]。代码如下:

private void getPalindrome(boolean[][] isPalindrome, String s){

    for(int i = 0; i < s.length(); i++){

        for(int j = i; j >= 0; j--){

            if(i == j){
                // s[i] == s[j]
                isPalindrome[j][i] = true;
                continue;
            }

            if(i - j == 1){
                // s[i] 与 s[j] 相邻
                isPalindrome[j][i] = s.charAt(i) == s.charAt(j) ? true : false;
                continue;
            }

            if(s.charAt(i) == s.charAt(j) && isPalindrome[j + 1][i - 1]){
                isPalindrome[j][i] = true;
            }else{
                isPalindrome[j][i] = false;
            }
        }
    }
}