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

这是我参与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,进入下一轮循环

还是使用如下框架代码:

1
2
3
4
5
6
7
8
9
10
java复制代码result = []
backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return

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

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码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%的用户

小结

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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%