- 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
1、问题抽象与代码实现
1)问题抽象
2)代码实现
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
/----回溯终止条件1:当前和大于给定值----/
if (sum > target) {
return;
}
/----回溯终止条件2:当前和等于给定值----/
if (sum == target) {
result.push_back(path);
return;
}
/-----回溯逻辑-----/
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, target, 0, 0);
return result;
}
};
2、剪枝优化
我们可以对给定数组进行排序,排序后,如果回溯某个元素时和大于给定值,则后面的元素由于均大于当前元素,也就不用再回溯了
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
return;
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
3、横向重复与纵向重复
纵向去重
题目中有以下两点要求:
- 给定的数组中无重复元素
这对应树结构中的横向层,给定的数组中无重复元素就是不需要横向去重 - 数组中的元素可以重复使用
这对应树结构中的纵向,当前数字使用过了还可以继续使用,因此回溯函数调用时候为
backtracking(candidates, target, sum, i);
如果要求数组中的元素不可以重复使用,则需要进行纵向去重,即使用过的数字不能再使用,此时回调函数调用为
backtracking(candidates, target, sum, i + 1);
纵向去重的关键在于当前节点调用递归函数时是否包括当前节点。如果包括当前节点,就没有去重;如过不包括当前节点,就纵向去重。
横向去重
当题目中给定的数组有重复元素又要求结果中不能有重复结果时,需要横向去重,如数组为[1,1,3,4],则排列[第一个1,3,4]和[第二个1,3,4]在结果中是重复的。
既然要横向去重,我们就要先对给定数组进行排序,从而根据元素是否与上一元素相等来确定该元素是否重复
我们使用一个与数组等长的vector<bool> used
来标志在当前层中,对应的数字是否已经被使用。那么在进行当前路径选择时,当某个数字满足i > 0 && nums[i] == nums[i - 1] && used[i - 1]
时, 该数字就是重复的,这条路径就不应该再被选择,具体解释如图:
如图,used数组表明的是数字在该层之前是否被使用,i > 0 && nums[i] == nums[i - 1] && used[i - 1]
的含义是对于index = i的元素,在他之前有一个元素i,且该元素没有在该层之前被使用过;也就是说index = i - 1这个元素会在本层被使用,且在index = i的元素之前被使用,那么我们再使用index = i就是横向重复了,因此在本层for循环中要跳过index = i的元素。而对于首元素,肯定不重复,不用判断,同时i > 0一定要放在条件最前面,放置越界。
组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
横向去重流程
- 数组排序
- 初始化used数组
- 当将某个元素添加到路径时,将其used设置为true,表明被使用了
- 使用
nums[i] == nums[i - 1] && used[i - 1]
判定是否同层重复
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
if (sum == target) {
result.push_back(path);
return;
}
/---当前层横向循环遍历----/
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
/----判定当前元素是否同层重复,如果是,跳过该元素-----/
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
continue;
}
/----如果不是,正常递归----/
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true; //使用了i元素,更改其used状态
backtracking(candidates, target, sum, i + 1, used);
used[i] = false; //回溯时候回溯i的used状态
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
path.clear();
result.clear();
/-----初始化used数组-----/
vector<bool> used(candidates.size(), false);
/-----数组排序----/
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
不排序的横向去重
491、给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2 。
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
结果中不应该包含重复的递增子序列
如上,题目需要横向去重,但我们不能对数组进行排序,否则数据就乱了。这时候我们可以使用set来进行去重。
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
void backTracking(vector<int>& nums, int startIndex){
if(path.size() > 1) res.push_back(path);
/-----set去重步骤1:在每个回调中(即每层遍历中)新建一个set来记录选择过的路径-----/
unordered_set<int> numSet;
for(int i = startIndex; i < nums.size(); i++){
if(path.size() != 0 && nums[i] < path.back()) continue;
/-----set去重步骤2:如果当前路径被set记录过,跳过,横向去重----/
if(numSet.find(nums[i]) != numSet.end()) continue;
/-----set去重步骤3:如果当前路径没有被set记录过,则使用set记录该路径-----/
/-----注意该添加到set的操作不要回溯,因为这里记录的是曾经选择过该路径-----/
numSet.insert(nums[i]);
path.push_back(nums[i]);
backTracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
res.clear();
path.clear();
backTracking(nums, 0);
return res;
}
};
两种横向去重性能分析:在可以使用排序去重的时候不要使用set去重,set去重由于set多层压栈会显著增加运行时间和内存占用
-
主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的
-
如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合
-
而used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)