给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
3.28 自己写的
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> res=new ArrayList<>();
helper(candidates, res, new ArrayList<>(), 0,target);
return res;
}
private void helper(int[] num,List<List<Integer>> res, List<Integer> list, int start, int target){
if(target<0) return;
if(target==0){
res.add(new ArrayList<>(list));
return ;
}
for(int i=start;i<num.length;i++){
if(num[i]>target) break;
list.add(num[i]);
target-=num[i];
helper(num, res, list, i,target);
list.remove(list.size()-1);
target+=num[i];
}
}
}
回溯法 剪枝
解题思路:
做搜索、回溯问题的套路是画图,代码其实就是根据画出的树形图写出来的。
那么如何画图呢?
根据题目中的用例,画一个图,因为是搜索,因此呈现的是一个树形结构图,并且在这个树形结构中会体现出递归结构。
根据题目中的用例,比对自己画图的结果和题目的结果的差异,如果一样,说明我们的分析没有错;如果不一样,说明我们的分析有误,一定有哪一个环节漏掉了或者分析错误,根据找到的问题调整算法。
-
一个蓝色正方形表示的是 “尝试将这个数到数组 candidates 中找组合”,那么怎么找呢?挨个减掉那些数就可以了。
-
在减的过程中,会得到 0 和负数,也就是被我标红色和粉色的结点:
得到 0是我们喜欢的,从 0这一点向根结点走的路径(很可能只走过一条边,也算一个路径),就是一个组合,在这一点要做一次结算(把根结点到 0 所经过的路径,加入结果集)。
得到负数就说明这条路走不通,没有必要再走下去了。
根据回溯 剪枝 可写出简易版:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
if(candidates.length==0 || target<=0) return res;
helper(candidates, target, 0, new ArrayList<>(), res);
return res;
}
public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
if(target<0) return;
if(target==0){
res.add(new ArrayList<>(list));
return ;
}
for(int i=start;i<candidates.length;i++){
list.add(candidates[i]);
helper(candidates , target-candidates[i], i, list, res);
list.remove(list.size()-1);
}
}
}
画出图以后,我看了一下,我这张图画出的结果有 44 个 00,对应的路径是 [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]],而示例中的解集只有 [[7], [2, 2, 3]],很显然,我的分析出现了问题。问题是很显然的,我的结果集出现了重复。重复的原因是
后面分支的更深层的边出现了前面分支低层的边的值。
限于我的表达能力有限,大伙意会这句话就可以了,看一看重复的叶子结点 00 的路径,想一想重复的原因,或许你会比我说得更清楚更好。
但是这个问题也不难解决,把候选数组排个序就好了(想一下,结果数组排个序是不是也可以去重),后面选取的数不能比前面选的数还要小,即 “更深层的边上的数值不能比它上层的边上的数值小”,按照这种策略,剪枝就可以去掉重复的组合。
补充:事实上,不排序也是可以的,只要保证按顺序读取,也可以通过测试用例,我个人还是觉得排序更好一些,这样“剪枝”工作可以更彻底一些。
优化后代码:
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
if(candidates.length==0 || target<=0) return res;
// 优化添加的代码1:先对数组排序,可以提前终止判断
Arrays.sort(candidates);
helper(candidates, target, 0, new ArrayList<>(), res);
return res;
}
public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
if(target<0) return;
if(target==0){
res.add(new ArrayList<>(list));
return ;
}
// 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
// residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
// 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
for(int i=start;i<candidates.length && target-candidates[i]>=0;i++){
list.add(candidates[i]);
// 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
helper(candidates , target-candidates[i], i, list, res);
list.remove(list.size()-1);
}
}
}
lee40 组合总和2
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
与上一题的区别:
就是 不可重复
在递归时,递归函数参数 变成 i+1
在判断是否重复时怎么办?
在for循环递归中 剪枝
if(target==0 && !res.contains(list)){
res.add(new ArrayList<>(list));
return ;
}
这样还是比较费事
还可以这样
这个方法最重要的作用是
可以让同一层级,不出现相同的元素。但是却允许了不同层级之间的重复
为何会有这种神奇的效果呢?
首先 cur-1 == cur 是用于判定当前元素是否和之前元素相同的语句。这个语句就能砍掉例1。可是问题来了,如果把所有当前与之前一个元素相同的都砍掉,那么例二的情况也会消失。 因为当第二个2出现的时候,他就和前一个2相同了。
那么如何保留例2呢?
那么就用cur > begin 来避免这种情况,你发现例1中的两个2是处在同一个层级上的,
例2的两个2是处在不同层级上的。在一个for循环中,所有被遍历到的数都是属于一个层级的 ,我们要让一个层级中,必须出现且只出现一个2,那么就放过第一个出现重复的2,但不放过后面出现的2。第一个出现的2的特点就是 cur == begin. 第二个出现的2 特点是cur > begin.
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res=new ArrayList<>();
if(candidates.length==0 || target<=0) return res;
// 优化添加的代码1:先对数组排序,可以提前终止判断
Arrays.sort(candidates);
helper(candidates, target, 0, new ArrayList<>(), res);
return res;
}
public void helper(int[] candidates, int target, int start,List<Integer> list, List<List<Integer>> res){
if(target<0) return;
if(target==0 ){
res.add(new ArrayList<>(list));
return ;
}
// 优化添加的代码2:在循环的时候做判断,尽量避免系统栈的深度
// residue - candidates[i] 表示下一轮的剩余,如果下一轮的剩余都小于 0 ,就没有必要进行后面的循环了
// 这一点基于原始数组是排序数组的前提,因为如果计算后面的剩余,只会越来越小
for(int i=start;i<candidates.length && target-candidates[i]>=0;i++){
if(i>start && candidates[i-1]==candidates[i]){
continue;
}
list.add(candidates[i]);
// 【关键】因为元素可以重复使用,这里递归传递下去的是 i 而不是 i + 1
helper(candidates , target-candidates[i], i+1, list, res);
list.remove(list.size()-1);
}
}
}
这样立马性能提升很多
!!!嘿嘿
3.28自己写的
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> res=new ArrayList<>();
helper(candidates, res, new ArrayList<>(), 0,target);
return res;
}
private void helper(int[] num,List<List<Integer>> res, List<Integer> list, int start, int target){
if(target<0) return;
if(target==0){
res.add(new ArrayList<>(list));
return ;
}
for(int i=start;i<num.length;i++){
if(num[i]>target) break;
if(i>start && num[i]==num[i-1]) continue;
list.add(num[i]);
target-=num[i];
helper(num, res, list, i+1,target);
list.remove(list.size()-1);
target+=num[i];
}
}
}
lee216 组合总和3
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
3.28 自己写的
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res=new ArrayList<>();
if(n<=0 || k<=0 ) return res;
helper(k,n,res, new ArrayList<>(), 1);
return res;
}
private void helper(int k, int n, List<List<Integer>> res, List<Integer> list, int start){
if(n<0) return ;
if(list.size()==k && n==0 ){
res.add(new ArrayList<>(list));
return ;
}
for(int i=start;i<=9;i++){
list.add(i);
helper(k,n-i,res, list, i+1);
list.remove(list.size()-1);
}
}
}
class Solution {
public List<List<Integer>> combinationSum3(int k, int n) {
List<List<Integer>> res=new ArrayList<>();
if(k<=0 || k>9 || n<=0) return res;
helper(k , n, res , new ArrayList<Integer>(),1);
return res;
}
public void helper(int k, int n ,List<List<Integer>> res,List<Integer> list, int start){
if(n<0) return;
if(n==0 && list.size()==k){
res.add(new ArrayList<>(list));
return;
}
for(int i=start;i<=9 ;i++){
if(n-i<0) continue;//加上这句 性能提高到70%
list.add(i);
helper(k, n-i ,res, list, i+1);
list.remove(list.size()-1);
}
}
}