给定一个无重复元素的数组 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]
]
思路分析:拿到这道题的时候,我首先想起的是前些时候做过的三数之和、四数之和等题型(如果没有做过,请参考我之前的博客)。但是此题与之前的题型稍有不同,之前的题都是确定好了数的个数,而此道题与数的个数无关,主要是要凑齐target。对于这种题型,比较常用的解法是“回溯法”(深度优先搜索)。即从数组中不断拿出元素进行凑,如果不合格就退回到上一步,合格就继续下一步。
初始代码如下:
class Solution {
public:
set<vector<int> > resultSet;//采取集合的形式方便去重
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
resultSet.clear();//首先对结果集合进行清空
int candidateSize = candidates.size();
if (candidateSize == 0){
return vector<vector<int>>(resultSet.begin(), resultSet.end());//返回结果为vector,需要转换
}
sort(candidates.begin(), candidates.end());//进行排序处理,方便选取
vector<int> tempResult;//中间结果
searchDSF(candidates, tempResult, target);//进行搜索
return vector<vector<int>>(resultSet.begin(), resultSet.end());
}
//candidates是候选的元素,tempResult表示中间结果,target表示此时还需要凑的数字
void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target){
if (target == 0){//如果求和成功
vector<int> tempVector = tempResult;//1⃣️再次复制一下,需要进行排序处理
sort(tempVector.begin(), tempVector.end());//将中间结果进行排序,方便去重
resultSet.insert(tempVector);//放入结果集合中
}
else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
int candidateSize = candidates.size();
//对候选元素进行穷举
for (int i = 0; i < candidateSize; ++i){
if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
tempResult.push_back(candidates[i]);//放入中间结果
searchDSF(candidates, tempResult, target - candidates[i]);//继续搜寻target -candidates[i]
tempResult.pop_back();//搜索之后,需要把之前的删除,这就显示1⃣️操作的重要性了,否则排序后,尾端不一定是前面放进去的那个元素
}
}
}
}
};
代码优化:上面的代码对于重复的结果需要靠set的元素的唯一性,才能进行去重,由于两个vector容器只有当元素值、顺序完全一样才认为是一样的,所以每次得到一个正确的结果都需要排序,才能去重。那我们能不能在源头上就不让它产生重复选项,这样就不需要去重处理?(请思考一下)
举个栗子:
第一种情况:第一次选择了2,第二次选择了3,第三次…
第二种情况:第一次选择了3,第二次选择了3,第三次…
经过排序处理后的vector容器,完全是一样的。这时我们就要想起,在进行深搜之前,对candidates容器进行了排序,那我们能不能对放入的元素采取非递减的规则。这样就可以缩减了去重的过程。
优化后的代码:
class Solution {
public:
set<vector<int> > resultSet;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
resultSet.clear();//清空
int candidateSize = candidates.size();
if (candidateSize == 0){
return vector<vector<int>>(resultSet.begin(), resultSet.end());
}
sort(candidates.begin(), candidates.end());//进行排序处理
vector<int> tempResult;//中间结果
searchDSF(candidates, tempResult, target, 0);//进行搜索
return vector<vector<int>>(resultSet.begin(), resultSet.end());
}
//beginIndex用于标记之前使用过的最大的下标
void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target, int beginIndex){
if (target == 0){//如果求和成功
resultSet.insert(tempResult);//放入结果集合中
}
else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
int candidateSize = candidates.size();
//对候选元素进行穷举
//beginIndex用于标记之前使用过的最大的下标,从这个下标开始才能保持f,放入中间结果的元素非递减的顺序
for (int i = beginIndex; i < candidateSize; ++i){
if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
tempResult.push_back(candidates[i]);//放入中间结果
searchDSF(candidates, tempResult, target - candidates[i], i);//继续搜寻target -candidates[i]
tempResult.pop_back();
}
}
}
}
};
但是貌似还忘记了啥!!!既然此时已重源头上避免的重复中间结果的出现,那么我们现在为啥还需要使用set容器,而不直接使用vector容器进行储存呢?
下面的代码将set容器修改为vector容器
class Solution {
public:
vector<vector<int> > resultVec;//set容器修改为vector容器
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
resultVec.clear();
int candidateSize = candidates.size();
if (candidateSize == 0){
return resultVec;
}
sort(candidates.begin(), candidates.end());//进行排序处理
vector<int> tempResult;//中间结果
searchDSF(candidates, tempResult, target, 0);//进行搜索
return resultVec;
}
void searchDSF(vector<int>& candidates, vector<int> &tempResult, int target, int beginIndex){
if (target == 0){//如果求和成功
resultVec.push_back(tempResult);//放入结果集合中
}
else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
int candidateSize = candidates.size();
//对候选元素进行穷举
for (int i = beginIndex; i < candidateSize; ++i){
if (candidates[i] <= target){//如果这个数小于target,则说明能够使用
tempResult.push_back(candidates[i]);//放入中间结果
searchDSF(candidates, tempResult, target - candidates[i], i);//继续搜寻target -candidates[i]
tempResult.pop_back();
}
}
}
}
};
可能有不少师兄、弟知道“回溯法”(深度优先搜索法)时间复杂度一般都比较大,需要采取一些“剪枝算法”进行辅助,(有时候一个好的剪枝算法可以大大的降低整个算法的时间复杂度),但是我的代码貌似没有见到呀!
请回到searchDSF函数中的
else if (target >= candidates[0]){//只能当还有元素可以被选入的情况下进行
这句算法就是“剪枝算法”,含义就是,只有当仍需要凑齐的target大于候选容器中最小的元素,才可能继续进行搜索。
但是这句还可以改成
else if (target >= candidates[beginIndex]){//candidates[beginIndex]表示的上一次之前最大的元素,由于之前从源头上去重的算法采取非递减策略,所以仍需要凑齐的数必须要大于等于使用过的最大的数,才能继续进行凑。
由于领扣中国的测试数据量比较小,我更换剪枝算法还是8ms,看不出效果,但是千万别小看“剪枝算法”的使用在使用“回溯法”时。