题干:
Given a set of candidate numbers (C) and a target number (T), find all unique combinations in C where the candidate numbers sums to T.
The same repeated number may be chosen from C unlimited number of times.
Note:
All numbers (including target) will be positive integers.
Elements in a combination (a1, a2, … , ak) must be in non-descending order. (ie, a1 ≤ a2 ≤ … ≤ ak).
The solution set must not contain duplicate combinations.
For example, given candidate set 2,3,6,7 and target 7,
A solution set is:
[7]
[2, 2, 3]
翻译:
给定一个待选数据的集合candidate和一个目标target,寻找所有和为target的子集合,并且不能有重复,要求:子集合必须单调不减,candidate中的元素可以重复使用
分析:
题目一眼看上去很像之间做过的sum系列,然而这里k不是固定的,没有确切要求几个元素的和是target,for循环+指针或者hash的方法显然不行,我们需要一种新的解题思路。再次回顾题目,如果我们把问题简化一下,不可以用重复元素,第一个元素要从candidate[0]开始,那么是不是就很像在一棵树中找到一条路径,让路径上的元素之和等于target呢?
想到这里,我们就可以引入今天利用的算法:回溯法
什么是回溯法?所谓回溯法,就是把求解空间抽象成一个图,然后从图中某一点出发,利用深度遍历找到所有满足条件的解
好了,我们再回到题目,加上我们之前分析时去掉的条件,那么就成了在一个图中从图中任何一个点出发,找到所有满足条件的路径,先上代码:
class Solution {
public:
vector<vector<int> > ret;
vector<int> curr;
public:
void search(vector<int>& candidates, int next, int target){
//返回条件
if (target == 0){
ret.push_back(curr);
return;
}
if (next == candidates.size() || target - candidates[next] < 0)
return;
//回溯递归部分
curr.push_back(candidates[next]);
search(candidates, next, target - candidates[next]);
curr.pop_back();
search(candidates, next + 1, target);
}
vector<vector<int> > combinationSum(vector<int> &candidates, int target){
sort(candidates.begin(), candidates.end());
search(candidates, 0, target);
return ret;
}
};
首先对数列进行排序,便于后面的处理,然后我们调用DFS函数search(),search()中前两个返回条件很简单,当我们发现target减到0时就找到了一个解,如果指针超出范围,或者加上下一个指针指向的值已经超过了target,就返回,下面我们看看最重要的递归部分:
curr.push_back(candidates[next]);
search(candidates, next, target - candidates[next]);
curr.pop_back();
search(candidates, next + 1, target);
首先我们把当前指针指向的元素放进curr中,然后向下一个元素走,由于题目要求元素可重复使用,这里next指针先原地踏步,因为已经找到一个元素,所以目标变成了target - candidates[next],然后进入下一个递归函数,如果没有超出范围就一直循环下去,直到出错或者找到一条路径,如果出错跳回上层循环,将错误元素弹出,递归下一个元素直到完成,文字说明并不明了,我们举个例子说明:
数列:[10,2,7,6,3,5]
目标:8
search1是search(candidates, next, target - candidates[next])
search2是search(candidates, next + 1, target)
⑴排序,数列变为2,3,5,6,7,10
⑵next=0,target=8,curr=NULL;第一次递归,达到回溯条件,回溯,进入search2
⑶next=1,target=8,curr=”2”;第二次递归,未达到回溯条件,继续 ,进入search1
⑷next=1,target=6,curr=”2,2”;第二次递归,未达到回溯条件,继续 ,进入search1
⑸next=1,target=4,curr=”2,2,2”;第二次递归,未达到回溯条件,继续 ,进入search1
⑹next=1,target=2,curr=”2,2,2,2”;第二次递归,未达到回溯条件,继续 ,进入search1
⑺next=1,target=0,curr=”2,2,2,2,2”;第二次递归,达到回溯条件,保存,回退
⑻next=2,target=2,curr=”2,2,2,2,7”;第二次递归,达到回溯条件,回退
⑼next=3,target=2,curr=”2,2,2,2,6”;第二次递归,达到回溯条件,回退
and so on….
程序就这样一步步找到所有解
其实回溯法用递归实现非常简便,而且语意明确,这里如果熟悉递归的话,我们知道这里一直递归是有一点问题的,如果出现极端情况目标非常大,而数列中有很小的元素,比如
数列:[1,2,7,6,3,5]
目标:1000
这样,那么看看我们的递归程序会怎么样?一直递归1000遍!才能找到一个解,我们知道,递归程序每递归一次就会用栈保存上一个递归的现场以便返回,这样递归栈就会非常非常长,为了解决这个问题我们并不需要完全改成非递归,只要利用一次循环避免这样的极端情况:
class Solution {
vector<vector<int>> ret;
vector<int> curr;
public:
void backtrack(vector<int>& candidates, int target, int i,int sum) {
if (i < 0 || i >= candidates.size() || sum > target) return;
if (sum == target) {
ret.push_back(curr);
return;
}
//避免极端情况
for (; i < candidates.size(); i++) {
curr.push_back(candidates[i]);
backtrack(candidates, target, i, sum + candidates[i]);
curr.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if (candidates.empty() || target == 0)
return ret;
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0, 0);
return ret;
}
};
思路基本上是一模一样的,只是利用一个for循环代替左指针右移的过程,实际就是代替search(candidates, next + 1, target);这步递归
但是有点要说明,实际上非极端情况下第一种解法的效率较高