代码随想录算法训练营第二十六天 | 组合总和 I、II、分割回文串

39. 组合总和

leetcode
文档题解
视频题解

画出决策图。每次都从待选择集合中拿出一个元素放进 path,并累加进 sum,然后递归做出第二次决策。对于每一次决策,如果 sum == target,则得到一个解,并放进结果集。因为这次要求 path 中可选择重复的元素,例如 target 为 4,path 可以是 [1, 1, 1, 1],因此每次决策中 for 的起始下标(startIndex)可以和上次决策中的一致。

又因为求的是组合问题。[1, 2, 3] 和 [2, 1, 3] 是一样的,因此需要控制 for 的起始遍历顺序,于是引入了 startIndex 变量。

举个例子,假设 target 是 4,第一次第一层决策选择了 1,那么产生的结果有 [1, 1, 1, 1],[1, 1, 2],[1, 2, 1],[1, 3],第二次第一层决策时选择了 2,那么第二次第二层决策就不能选择 1,因为第一次决策时已经把 1、2 的情况讨论过了,该有的解都有了,再来一遍就会重复,因此后续的几次决策要把前面做决策的值排除掉,所以组合问题一般需要 for 循环从指定的位置开始遍历,即 startIndex。

class Solution {
    private List<List<Integer>> res;
    private LinkedList<Integer> path;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        res = new LinkedList<>();
        path = new LinkedList<>();
        backtrace(candidates, target, 0);
        return res;
    }
    public void backtrace(int[] candidates, int target, int startIndex) {
        // 下面有优化,这里就不需要了
        // if (target < 0) return;
        if (target == 0) {
            // 找到一个解
            res.add(new LinkedList<>(path));
            return;
        }

        for (int i = startIndex; i < candidates.length; i++) {
            // 优化手段1:已知当前数值相加会大于 target,就没必要进入下一次递归了(虽然下一次递归会立刻返回,但是会多一层递归)
            // 优化手段2:如果 candidates 是个有序数组,那么当遇到当前数值相加大于 target 时,后续的数值就不需要考虑了,直接 break,更省事
            if (target - candidates[i] < 0) continue;
            path.add(candidates[i]);
            backtrace(candidates, target - candidates[i], i);
            path.removeLast();
        }
    }
}

40. 组合总和 II

leetcode
文档题解
视频题解

class Solution {
    private List<List<Integer>> res;
    private LinkedList<Integer> path;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        res = new LinkedList<>();
        path = new LinkedList<>();
        // 去重的关键 1
        // 剪枝的关键
        Arrays.sort(candidates);
        backtrace(candidates, target, 0);
        return res;
    }
    public void backtrace(int[] candidates, int target, int startIndex) {
        // if (target < 0) return;
        if (target == 0) {
            res.add(new LinkedList<>(path));
            return;
        }

        // 剪枝
        for (int i = startIndex; i < candidates.length && target - candidates[i] >= 0; i++) {
            // 去重的关键 2
            if (i > startIndex && candidates[i] == candidates[i - 1]) continue;
            path.add(candidates[i]);
            backtrace(candidates, target - candidates[i], i + 1);
            path.removeLast();
        }
    }
}

131. 分割回文串

leetcode
文档题解
视频题解

这道题一开始没理解到底要怎样分割,看了 Carl 的解释才知道和组合的方式差不多。例如字符串是 “aab”,一开始切分第一个 “a”,是回文串,然后再切分剩下的字符串 “ab”,切分第二个 “a”,也是回文串,继续往下(递归)切分 “b”,发现也是回文串,此时切到底了,将这三个结果收集起来。然后第二次切就从第二个 “a” 开始,切出来的是 “aa”,是回文串,剩下 “b”,再切 “b”,也是回文串,切到底了,收集起来。然后第三次切就从 “b” 开始,切出来的是 “aab”,不是回文串,切到底了,结束。于是就有了答案:[["a","a","b"],["aa","b"]]。

private List<List<String>> res;
    private LinkedList<String> path;
    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        path = new LinkedList<>();
        backtrace(s, 0);
        return res;
    }
    public void backtrace(String s, int startIndex) {
        if (startIndex >= s.length()) {
            res.add(new LinkedList<>(path));
            return;
        }

        for (int i = startIndex; i < s.length(); i++) {
            String str = s.substring(startIndex, i + 1);
            // 是回文串,才加入 path,继续向下切分
            if (isPalindrome(str)) {
                path.add(str);
                backtrace(s, i + 1);
                path.removeLast();
            }
        }
    }
    public boolean isPalindrome(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) return false;
            left++;
            right--;
        }
        return true;
    }

何时使用 startIndex,何时不用

这里讲的用或不用指的是在 for 循环中 i 的初始化那里。

一般来说,如果是 组合 题目,且要求从一个集合里面做选择,就需要使用 startIndex,用于对最终结果的 去重。如果是排列的题目,允许有重复的序列,所以 for 循环中 i 的初始化从 0 开始,而不是 startIndex。

参考资料

回溯算法 + 剪枝(回溯经典例题详解)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注