问题描述:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
例:输入: candidates = [2,3,6,7], target = 7。所求解集为:[ [7], [2,2,3] ]
注:
- candidates 中的数字可以无限制重复被选取
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合
思路:
- 递归回溯
通过递归回溯找到所有满足的组合。
(图片来源于leetcode用户liweiwei1419)
时间复杂度:o(n2)
空间复杂度:o(n)
- 动态规划
此问题可以看成动态规划问题,对小于target的元素先进行寻找
//java
//遍历回溯
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
if(len == 0) return list;//数组长度为0,直接返回空列表
Arrays.sort(candidates);//排序去重,不能选比前面选择还要小的元素
findTargetSum(candidates, 0, target, new Stack<>());//以target作为回溯根节点进行回溯递归
return list;
}
public void findTargetSum(int[] candidates, int start, int residue, Stack<Integer> temp){
if(residue == 0){//找到可满足序列
list.add(new ArrayList<>(temp));//将栈中元素加入列表
}else if(residue > 0){//继续回溯
for(int i = start; i < candidates.length && residue - candidates[i] >= 0; ++i){//从start开始向后进行回溯寻找,中间判断将减到<0的情况剔除
temp.push(candidates[i]);//回溯元素入栈,作为可能满足的数字序列
findTargetSum(candidates, i, residue - candidates[i], temp);//由于单个数字可取多次,对当前字符回溯
temp.pop();//出栈
}
}
}
}
//动态规划
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
int len = candidates.length;
if(len == 0) return list;
Arrays.sort(candidates);
Map<Integer, Set<List<Integer>>> imap = new HashMap();//值设置为集合,保证后面加入元素时不重复
for(int i = 1; i <= target; ++i){//从1到target开始每个建立可满足集合
imap.put(i, new HashSet<>());//初始化各元素可满足集合
for(int j = 0; j < len && candidates[j] <= target; j++){//遍历数组,对每个小于i的元素进行减值判断
if(i == candidates[j]){//剪完恰好为0,填入i对应的集合
imap.get(i).add(new ArrayList<>(Arrays.asList(i)));
}else if(i > candidates[j]){
int key = i - candidates[j];
for(Iterator iterator = imap.get(key).iterator(); iterator.hasNext();){//关键部分,体现了动态规划的特点,对于减完的元素其满足情况对应于该较小值的可满足集合,由于在之前已经记录,仅将减去的candidates[j]值补充到原来的集合序列后面
List tempList = new ArrayList<>((List)iterator.next());
tempList.add(candidates[j]);//补充元素
Collections.sort(tempList);//排序保证不重复
imap.get(i).add(tempList);//集合的add方法会先判断元素是否已经存在在集合中,若存在将不会加入进去
}
}else
break;
}
}
list.addAll(imap.get(target));//获取target的满足集合
return list;
}
}
扩展:把题中条件改为数组中每个数字只能使用一次,并且数组中可能有重复数字。
思路:解决思路同之前一样,但由于每个数字只能取一次,在递归回溯时需要对下一数字进行回溯,同时,由于有重复字符,在回溯时需要跳过相同字符的回溯。
注:由于原题中的动态规划思想是利用了子结构的性质,这里由于数字不能重复取,且存在重复元素,导致无法从子结构推出当前结构的组成(子结构中所取元素可能会与当前重复),所以上述第二种方法不适用。
class Solution {
List<List<Integer>> list = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null || candidates.length == 0) return list;
Arrays.sort(candidates);
findTargetSum(candidates, 0, target, new Stack<>());
return list;
}
public void findTargetSum(int[] candidates, int start, int residue, Stack<Integer> temp){
if(residue == 0){
list.add(new ArrayList<>(temp));
}else if(residue > 0){
for(int i = start; i < candidates.length && residue >= candidates[i]; ++i){
if(i == start || candidates[i] != candidates[i-1]){//改动之处:当前字符为回溯每一层首先所考虑字符或当前字符与上一字符不同,进行回溯(因为如果不为首考虑字符且与前面字符相同,对该字符的后续回溯会与前一相同字符的部分相同)
temp.push(candidates[i]);
findTargetSum(candidates, i+1, residue-candidates[i], temp);
temp.pop();
}
}
}
}
}