【算法攻坚】回溯模板解题

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

今日题目

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

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

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

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5, 输出: [ [1,2,2], [5] ]

提示:

1 <= candidates.length <= 100 1 <= candidates[i] <= 50 1 <= target <= 30

思路

这道题相较于组合总和1的区别是:

  • 1、每个元素不能再无限使用,而是只能用一次,因此每次递归的下标不能再从当前小标开始,而是i+1开始。
  • 2、元素有重复元素,因此需要去重。

上述两点都有去重的效果,一个是在结果上保证了没有重复元素,一个是保证在横向遍历的时候保证没有重复搜索。 除此之外与大多数回溯思路一样。

递归遍历数组,每次首先判断当前下标元素是否与前一个元素相同,如果相同同时判断上一个元素是否正在使用,如果没有使用则直接跳过,因为上一个元素前面已经遍历过了。

然后判断当前元素是否小于等于target,如果大于target,则说明如果target减去当前元素差一定小于0,不满足等于0,直接break循环就可以了。因为该元素之后的元素一定是大于等于当前元素的,因此target减去该元素的差也一定是小于0的,所以直接跳过。这是一步非常重要的剪枝操作。

将当前元素加入path列表,将当前vis[i]设置为true表示已访问。

进入递归,判断target是否为0,如果为0,加入答案列表。

出递归,需要进行回溯,将当前元素从path列表中删除,并将访问置为false。

总结成两个问题

  • 1.如何保证每个数字在每个组合中只使用一次? 答:每次递归调用方法时,从当前数的下一个数开始
  • 2.如何保证解集不能包含重复的组合? 答:在有序数组中,如果索引i处的数字 等于 索引i-1处的数字,就直接跳过i,进入下一轮循环

还是使用如下框架代码:

result = []
backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

for 选择 in 选择列表:
    做选择
    backtrack(路径, 选择列表)
    撤销选择
复制代码

代码实现:

List<List<Integer>> result = new LinkedList<>();

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    List<Integer> list = new ArrayList<>();
    Arrays.sort(candidates);
    combinationList(candidates, 0, target, list);
    return result;
}

public void combinationList(int[] candidates, int start, int target, List<Integer> list{
    if (target == 0) {
        result.add(new ArrayList<>(list));
        return;
    }
    for (int i = start; i < candidates.length; i++) {
        // 剪裁情况一
        if (target - candidates[i] < 0) {
            break;
        }
        // 剪裁情况二
        if (i > start && candidates[i] == candidates[i - 1]) {
            continue;
        }
        list.add(candidates[i]);
        // 此处为i+1,因为元素不可重复使用,当用start+1时,会导致重复计算
        combinationList(candidates, i + 1, target - candidates[i], list);
        // 回溯
        list.removeLast();
    }
}
复制代码

执行用时:2 ms, 在所有 Java 提交中击败了99.32%的用户

内存消耗:38.4 MB, 在所有 Java 提交中击败了85.81%的用户

小结

回溯算法解题整体都是有着类似的目的,都是在寻找所有可行解的题,我们都可以尝试用搜索回溯的方法来解决。同时要记住算法实现的基本框架,可以大大减少实现难度。

猜你喜欢

转载自juejin.im/post/7035643297909440520