本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。
为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。
由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。
给定一个候选人编号的集合 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = `[10,1,2,7,6,1,5]`, target = `8`,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
提示:
1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30
由于我们需要求出所有和为 t a r g e t target target 的组合,并且每个数只能使用一次,因此我们可以使用递归 + 回溯的方法来解决这个问题:
我们用 d f s ( p o s , r e s t ) dfs(pos,rest) dfs(pos,rest) 表示递归的函数,其中 p o s pos pos 表示我们当前递归到了数组 c a n d i d a t e s candidates candidatess 中的第 p o s pos pos 个数,而 r e s t rest rest 表示我们还需要选择和为 r e s t rest rest 的数放入列表作为一个组合;
对于当前的第 p o s pos pos 个数,我们有两种方法:选或者不选。
在某次递归开始前,如果 r e s t rest rest 的值为 0 0 0 ,说明我们找到了一个和为 t a r g e t target target 的组合,将其放入答案中。每次调用递归函数前,如果我们选了那个数,就需要将其放入列表的末尾,该列表中存储了我们选的所有数。在回溯时,如果我们选了那个数,就要将其从列表的末尾删除。
上述算法就是一个标准的递归 + 回溯算法,但是它并不适用于本题。这是因为题目描述中规定了解集不能包含重复的组合,而上述的算法中并没有去除重复的组合。例如当 c a n d i d a t e s = [ 2 , 2 ] candidates=[2,2] candidates=[2,2] , t a r g e t = 2 target=2 target=2 时,上述算法会将列表 [ 2 ] [2] [2] 放入答案两次。
因此,我们需要改进上述算法,在求出组合的过程中就进行去重的操作。我们可以考虑将相同的数放在一起进行处理,也就是说,如果数 x x x 出现了 y y y 次,那么在递归时一次性地处理它们,即分别调用选择 0 , 1 , ⋯ , y 0,1,⋯ ,y 0,1,⋯ ,y 次 x x x 的递归函数。这样我们就不会得到重复的组合。具体地:
这样一来,我们就可以不重复地枚举所有的组合了。
我们还可以进行什么优化(剪枝)呢?一种比较常用的优化方法是,我们将 f r e q freq freq 根据数从小到大排序,这样我们在递归时会先选择小的数,再选择大的数(同[[LeetCode 39. Combination Sum【回溯,剪枝】中等]]的剪枝优化一样)。这样做的好处是,当我们递归到 d f s ( p o s , r e s t ) dfs(pos,rest) dfs(pos,rest) 时,如果 f r e q [ p o s ] [ 0 ] freq[pos][0] freq[pos][0] 已经大于 r e s t rest rest,那么后面还没有递归到的数也都大于 r e s t rest rest ,这就说明不可能再选择若干个和为 r e s t rest rest 的数放入列表了。此时,我们就可以直接回溯。
class Solution {
public:
vector<pair<int, int>> freq;
vector<vector<int>> ans;
vector<int> seq;
void dfs(int pos, int rest) {
if (rest == 0) {
ans.push_back(seq);
return;
}
if (pos == freq.size() || rest < freq[pos].first) return;
// 直接跳过
dfs(pos + 1, rest);
int most = min(rest / freq[pos].first, freq[pos].second);
for (int i = 1; i <= most; ++i) {
seq.push_back(freq[pos].first);
dfs(pos + 1, rest - i * freq[pos].first);
}
for (int i = 1; i <= most; ++i) seq.pop_back();
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
for (int num : candidates) {
if (freq.empty() || num != freq.back().first)
freq.emplace_back(num, 1);
else ++freq.back().second;
}
dfs(0, target);
return ans;
}
};
复杂度分析: