39. 组合总和 ●●
描述
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
题解
1. 回溯 + 排序剪枝
特点:
– 组合没有数量要求
– 元素可无限重复选取
因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回。
参数:遍历索引、数组、目标差值
终止条件:差值等于0
单层逻辑:for 循环从 index 开始,遍历数组集合(对数组排序可以减少回溯次数)
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(int idx, vector<int>& candidates, int target){
if(target == 0){
ans.emplace_back(path);
return;
}
for(int i = idx; i < candidates.size(); ++i){
if(target - candidates[i] < 0) break; // 剪枝,不用往下递归
path.emplace_back(candidates[i]);
backtrack(i, candidates, target-candidates[i]); // 递归、隐式回溯,可重复取值,索引为 i
path.pop_back(); // 回溯
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 排序,用于剪枝
backtrack(0, candidates, target);
return ans;
}
};
40. 组合总和 II ●●
描述
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
题解
1. 回溯 + 去重
此题重点在于重复组合的去重操作,所谓去重,其实就是使用过的元素不能重复选取。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过(递归纵向遍历),一个维度是同一树层上使用过(for横向遍历,i > start)。
本题中,元素在同一个组合内是可以重复的,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。树层去重的话,需要对数组排序!
- 排序,使用
vector<bool> &used
数组进行重复使用标记
class Solution {
public:
vector<vector<int>> ans;
vector<int> set;
void backtrack(int index, vector<int> candidates, int difference, vector<bool> &used){
if(difference == 0){
ans.emplace_back(set);
return;
}
for(int i = index; i < candidates.size(); ++i){
// used[i - 1] == true, 说明 **同一树枝** candidates[i-1]使用过
// used[i - 1] == false,说明 **同一树层** candidates[i-1]使用过
// 要对同一树层使用过的元素进行跳过
if(i > 0 && candidates[i] == candidates[i-1] && used[i-1] == false){
continue;
}
difference -= candidates[i];
if(difference < 0){
break; // 剪枝操作
}else{
set.emplace_back(candidates[i]);
used[i] = true; // 树枝上使用过
backtrack(i+1, candidates, difference, used); // 递归 纵向遍历
used[i] = false; // 树层上使用过
set.pop_back(); // 回溯
difference += candidates[i]; // 回溯
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(), false);
backtrack(0, candidates, target, used);
return ans;
}
};
- 排序,直接在for循环中判断与上一个元素是否重复进行去重判断
注意判断条件, i > index
时才是横向遍历的条件,否则可能是递归调用导致的判断。
class Solution {
public:
vector<vector<int>> ans;
vector<int> set;
void backtrack(int index, vector<int> candidates, int difference){
if(difference == 0){
ans.emplace_back(set);
return;
}
for(int i = index; i < candidates.size(); ++i){
if(i > index && candidates[i] == candidates[i-1]){
continue; // 跳过同一层使用过的元素(for 横向遍历)
} // 注意判断条件, i > index 时才是横向遍历的条件,否则可能是递归调用的判断
difference -= candidates[i];
if(difference < 0){ // 剪枝操作
break;
}else{
set.emplace_back(candidates[i]);
backtrack(i+1, candidates, difference); // 递归 纵向遍历,索引为 i+1
set.pop_back(); // 回溯
difference += candidates[i]; // 回溯
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); //排序
backtrack(0, candidates, target);
return ans;
}
};
216. 组合总和 III ●●
描述
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字 1 到 9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
题解
1. 回溯
两处剪枝操作:
- 当前组合和比目标值大时;
- for 循环遍历的上限值剪枝。
- 时间复杂度: O ( ( M k ) × k ) O({M \choose k} \times k) O((kM)×k),其中 M 为集合的大小,本题中 M 固定为 9。一共有 ( M k ) M \choose k (kM)(个组合,每次判断需要的时间代价是 O(k)。
- 空间复杂度:O(M)。临时数组的空间代价是 O(k),递归栈空间的代价是 O(M),故空间复杂度为 O(M+k)=O(M).
class Solution {
public:
vector<vector<int>> ans;
vector<int> set;
void backtrack(int k, int start, int num){ // k为元素个数,start为for遍历起始值,num为与目标差值
if(num < 0) return; // 与目标差值num<0,即和过大,停止递归,剪枝
if(set.size() == k){ // k个数时,结束递归
if(num == 0) ans.emplace_back(set); // 若组合和满足条件,即差值num=0时,保存数组
return;
}
int m = 9-(k-set.size())+1; // 对for循环最大值进行剪枝优化
for(int i = start; i <= m; ++i){ // 比如size=0时,遍历到7就停止,因为从8开始凑不够三个数
set.emplace_back(i);
backtrack(k, i+1, num-i); // 递归
set.pop_back(); // 回溯
}
}
vector<vector<int>> combinationSum3(int k, int n) {
backtrack(k, 1 , n);
return ans;
}
};