1 题目
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是唯一的。
事例1
输入:candidates = [2,3,6,7], target = 7
输出:[[7],[2,2,3]]
事例2
输入:candidates = [2], target = 1
输出:[]
2 回溯法
回溯法的重要思想:通过枚举法,对所有可能性进行遍历。 但是枚举的顺序是 一条路走到黑,发现走到黑之后,退一步,再向前尝试没走过的路。直到所有路都试过。因此回溯法可以简单的理解为:走不通就退一步的方枚举法就叫回溯法。而这里回退点也叫做回溯点。
回溯法实现的三大技术关键点分别是:
- 一条路走到黑
- 回退一步
- 另寻他路
通过for循环和递归来实现上述所说的关键点
- for循环的作用在于另寻他路:用for循环可以实现一个路径选择器的功能,该路径选择器可以逐个选择当前节点下的所有可能往下走下去的分支路径。 例如:现在走到了节点 a a a , a a a 就像个十字路口,你从上面来到达了 a a a,可以继续向下走。若此时向下走的路有 i i i 条,那么你肯定要逐个的把这 i i i 条都试一遍才行。而for的作用就是可以让你逐个把所有向下的i个路径既不重复,也不缺失的都试一遍。
- 递归可以实现一条路走到黑和回退一步:一条路走到黑:递归意味着继续向着for给出的路径向下走一步。 如果我们把递归放在for循环内部,那么for每一次的循环,都在给出一个路径之后,进入递归,也就继续向下走了。直到递归出口(走无可走)为止。 那么这就是一条路走到黑的实现方法。 递归从递归出口出来之后,就会实现回退一步。
因此for循环和递归配合可以实现回溯:当递归从递归出口出来之后。上一层的for循环就会继续执行了。而for循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。这就是回溯。
3 回溯代码模板
def backward():
if (回溯点):# 这条路走到底的条件。也是递归出口
保存该结果
return
else:
for route in all_route_set : # 逐步选择当前节点下的所有可能route
if 剪枝条件:
剪枝前的操作
return #不继续往下走了,退回上层,换个路再走
else:#当前路径可能是条可行路径
保存当前数据 #向下走之前要记住已经走过这个节点了。例如push当前节点
self.backward() #递归发生,继续向下走一步了。
回朔清理 # 该节点下的所有路径都走完了,清理堆栈,准备下一个递归。例如弹出当前节点
4 解答
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(candidates); //排序
backward(candidates, target, res, 0, new ArrayList<Integer>());
return res;
}
private void backward(int[] candidates, int target, List<List<Integer>> res, int i, ArrayList<Integer> temp) {
//递归出口↓
if (target < 0) return;
if (target == 0) {
res.add(new ArrayList<>(temp));
return;
}
//递归出口↑
for (int start = i; start < candidates.length; start++) {
if (target < 0)
break;
temp.add(candidates[start]);
backward(candidates, target - candidates[start], res, start, temp);
temp.remove(temp.size() - 1);
}
}