组合总和问题
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
问题分析
本问题是一个组合然后求和的问题,本质上就是一个组合问题,找到总和等于目标值的组合,可判断为回溯问题,所有回溯问题都可以抽象为树结构,是在一棵树上的深度优先遍历,那么在这里我们先画出一个简单的树结构,再进一分析问题。
然后进行我们回溯的三个步骤:
-
1.回溯函数的终止条件
到达树的叶子节点就是终止条件,从图中我们可以看出当数的和等于target或者大于target时就到达了叶子节点,这里我们使用一个sum来保存数的和,一维数组path来保存子集,二维数组result来保存总的结果集,所以当sum>=target时就终止。
得到终止时的代码为://终止本次递归,不做任何操作 if(sum>target){ return; } //保存结果 if(sum==target){ result.add(new ArrayList<Integer>(path)); return; }
-
2.单层递归的过程
一般来说这里有三个基本步骤:
(1)for循环控制树的横向遍历
(2)递归控制纵向遍历
(3)最后进行回溯
我们再来看看这个树形结构,通过for循环从左到右遍历,然后通过递归函数不断调用自己来进行纵向遍历,当遇到叶子节点时就返回;由于我们选择的元素可以重复,所以每一层的递归都是从该元素本身开始,在这里需要一个标志告诉下一个递归从本身开始。
代码为:
//通过startIndex通知递归的下次开始原始为自己
for(int i=startIndex;i<candidates.lengt;i++){
path.add(candidates[i]); //保存节点值
sum+=candidates[i]; //节点值做加
backTracking(candidates,target,sum,i); //递归
sum-=candidates[i]; //回溯
path.remove(path.size()-1); //回溯
}
- 3.确定递归函数的参数和返回值
(1)参数:通过上面的分析,确定基本需要的参数有:数组 candidates 、目标数 target、sum(保存目前元素和)、startIndex(下一次递归元素的起始位置)
(2)返回值:void
整体代码
public class CombinationSum {
List<Integer> path=new ArrayList<>(); //子集
List<List<Integer>> result=new ArrayList<>(); //结果集
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracking1(candidates,target,0,0);
return result;
}
public void backTracking1(int[] candidates,int target,int sum,int startIndex){
//终止本次递归,不做任何操作
if(sum>target){
return;
}
//保存结果
if(sum==target){
result.add(new ArrayList<Integer>(path));
return;
}
//通过startIndex通知递归的下次开始原始为自己
for(int i=startIndex;sum+candidates[i]<=target&&i<candidates.length;i++){
path.add(candidates[i]); //保存节点值
sum+=candidates[i]; //节点值做加
backTracking1(candidates,target,sum,i); //递归
sum-=candidates[i]; //回溯
path.remove(path.size()-1); //回溯
}
}
}
剪枝操作
回溯法唯一提高效率的操作,顾名思义就是减掉一些没有必要的操作。
从图中我们可以看出很多情况的sum都大于了target,比如说是否可以在2+2再加3之前就判断后面的数加上总数会大于target,直接终止操作,把剩余的部分减掉呢?
我们先分析一下,如果第一个选2,第二个也选2,那么接下来可以选择2、3、4,但是选择3和4的时候总和就会大于target,这时就可以在for循环时增加一个判断条件,直接结束3后面的操作,也就是当sum+candidates[i]>target时结束本轮循环。
注意:这里的例子数组恰好是有序的,题目并没有说candidates数组为有序,如果为{2,3,4,1},我们从3开始就终止了,可是1却是可行的路径,所以我们要使用这个终止条件的前提是给candidates排好序。
实现剪枝的修改代码为:
//传入candidates之前,先给它排好序
Arrays.sort(candidates);
//for循环增加sum+candidates[i]<=target条件
for(int i=startIndex;sum+candidates[i]<=target&&i<candidates.length;i++){
.......
}
//因为这里的for循环已经排除了大于target的可能,所以可以删除之前的sum>target终止