Leetcode 39 Combination Sum
题目原文
Given a set of candidate numbers (C) (without duplicates) 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.
- 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]
]
题意分析
给一组候选数据集合以及一个目标,求候选集合中的不同组合,使得每种组合的和等于目标值。候选集合中没有重复的数,每种组合中可以重复选择同一个候选数,同一组合的不同排列算一种组合。
解法分析
五大常用算法思想为分治、贪心、动态规划、回溯、分支界限。本题采用回溯思想,下面先对回溯法进行简要介绍。
回溯法被称为“通用解题法”,用它可以系统地搜索所有解,它是一个系统性又具跳跃性的搜索算法,是暴力搜索的很大优化。回溯法的主要步骤如下:
- 针对所给问题,定义问题的解空间。
本题的解空间就是不同候选数组合的集合,注意同一个数可以重复出现。
- 确定易于搜索的解空间结构
通常解空间都是树的结构,一个是子集树,二是排列树,子集树用于解是一个集合的子集的情况,用0表示集合中一个元素没有出现在解中,用1表示出现在解中,由此构成一个二叉树,深度为集合的大小,叶子节点有2^n个,代表2^n个解;排列树用于解是集合的不同排列的情况,此时树就为n叉树,深度为集合的大小,叶子结点n!个。本题情况特殊,由于同一个元素可以出现多次,所以该解空间数不是二叉树,且深度不限于候选集合大小。
- 以深度优先搜索的方式搜索解空间树,并在搜索过程中用剪枝函数避免无效搜索
剪枝函数包括约束函数,如扩展节点层数到达n等;界限函数,如有的子树注定达不到最优解,就提前剪去。本题没有深度限制,约束为加上扩展节点后不能超过目标值,界限为(Target-sum)<minCandi,此时就进入同级的下一个子树。C++代码如下:
class Solution {
private:
vector<int> candi;//use this varibles to avoid parameter trans to recursive function backtracking
int n;
int Target;
int s=0;
int sum=0;
//int minCandi;//minValue of candidates
vector<int> temp;
vector<vector<int>> solutionSet;
public:
void backtracking(int t){
if(sum==Target){
solutionSet.push_back(temp);
return ;
}
for(int i=s;i<n;i++){
// cout<<"this layer"<<t<<" "<<"i="<<i<<" "<<"sum="<<sum<<" "<<endl;
s=i;//to avoid same solution
if((sum+candi[i])>Target)
continue;
else{
sum+=candi[i];
//cout<<sum<<endl;
temp.push_back(candi[i]);
backtracking(++t);
temp.pop_back();
sum-=candi[i];
}
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
Target=target;
candi=candidates;
//minCandi=candi[0];
/* for(auto cc:candi)
minCandi=min(minCandi,cc);*/
n=candi.size();
backtracking(1);
return solutionSet;
}
};
代码中回溯法用递归函数backtracking实现,为了避免给递归函数传递过多参数,可以将参数设定为类的变量。为了避免产生同一种组合的不同排列,在进行树搜索时,定义一个变量s,用于存储子树树根所选元素,其所有子树选择元素必须大于等于它。回溯的关键点就在
temp.push_back(candi[i]);
backtracking(++t);
temp.pop_back();
sum-=candi[i];
在找到解或没找到解回溯后,应将所选值pop掉,sum也得恢复成上一级状态。由于一个数不满足界限条件,即加上它之后超过target,则比它大的同级元素也不同再搜索了,由此可大大减小搜索次数,因此算法的改进版本如下:
class Solution {
private:
vector<int> candi;//use this varibles to avoid parameter trans to recursive function backtracking
int n;
int Target;
int s=0;
int sum=0;
int minCandi;//minValue of candidates
vector<int> temp;
vector<vector<int>> solutionSet;
public:
void backtracking(int t){
/* if(sum==Target){
solutionSet.push_back(temp);
return ;
}*/
for(int i=s;i<n;i++){
// cout<<"this layer"<<t<<" "<<"i="<<i<<" "<<"sum="<<sum<<" "<<endl;
s=i;//to avoid same solution
if((sum+candi[i])==Target){
temp.push_back(candi[i]);
solutionSet.push_back(temp);
temp.pop_back();
return;//如果candidate从小到大,则这一层调用有解,后面也不用遍历了
}
else if((Target-sum-candi[i])<minCandi||(sum+candi[i])>Target)
continue;
else{
sum+=candi[i];
//cout<<sum<<endl;
temp.push_back(candi[i]);
backtracking(++t);
temp.pop_back();
sum-=candi[i];
}
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
Target=target;
sort(candidates.begin(),candidates.end());
candi=candidates;
minCandi=candi[0];
/*for(auto cc:candi)
minCandi=min(minCandi,cc);*/
n=candi.size();
backtracking(1);
return solutionSet;
}
};
先对candidate进行由小到大排序,这样就能大大减小搜索次数,注意在判断和等于target时要先push再pop,确保每一次push都对应一次pop。