40 组合总和II
文章目录
- 40 组合总和II
- 题目
- 官方解法:回溯
- 思路与算法
- code
- Reference
题目
给定一个候选人编号的集合 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
官方解法:回溯
思路与算法
由于我们需要求出所有和为 target的组合,并且每个数只能使用一次,因此我们可以使用递归 + 回溯的方法来解决这个问题:
-
我们用 dfs(pos,rest) 表示递归的函数,其中 pos表示我们当前递归到了数组 candidates中的第 pos\textit{pos}pos 个数,而 rest表示我们还需要选择和为 rest 的数放入列表作为一个组合;
-
对于当前的第 pos个数,我们有两种方法:选或者不选。如果我们选了这个数,那么我们调用 dfs(pos+1,rest−candidates[pos]) 进行递归,注意这里必须满足 rest≥candidates[pos]。如果我们不选这个数,那么我们调用 dfs(pos+1,rest)进行递归;
-
在某次递归开始前,如果 rest 的值为 0,说明我们找到了一个和为 target的组合,将其放入答案中。每次调用递归函数前,如果我们选了那个数,就需要将其放入列表的末尾,该列表中存储了我们选的所有数。在回溯时,如果我们选了那个数,就要将其从列表的末尾删除。
上述算法就是一个标准的递归 + 回溯算法,但是它并不适用于本题。这是因为题目描述中规定了解集不能包含重复的组合,而上述的算法中并没有去除重复的组合。
例如当 candidates=[2,2],target=2 时,上述算法会将列表 [2][2][2] 放入答案两次。
因此,我们需要改进上述算法,在求出组合的过程中就进行去重的操作。我们可以考虑将相同的数放在一起进行处理,也就是说,如果数 x 出现了 y 次,那么在递归时一次性地处理它们,即分别调用选择 0,1,⋯ ,y次 x的递归函数。这样我们就不会得到重复的组合。具体地:
- 我们使用一个哈希映射(HashMap)统计数组 candidates 中每个数出现的次数。在统计完成之后,我们将结果放入一个列表 freq中,方便后续的递归使用。
- 列表 freq 的长度即为数组 candidates 中不同数的个数。其中的每一项对应着哈希映射中的一个键值对,即某个数以及它出现的次数。
- 在递归时,对于当前的第 pos 个数,它的值为
freq[pos][0]
,出现的次数为freq[pos][1]
,那么我们可以调用
dfs(pos+1,rest−i×freq[pos][0])
即我们选择了这个数 iii 次。这里 iii 不能大于这个数出现的次数,并且i×freq[pos][0]
也不能大于 rest\textit{rest}rest。同时,我们需要将 i个 freq[pos][0]
放入列表中。
这样一来,我们就可以不重复地枚举所有的组合了。
我们还可以进行什么优化(剪枝)呢?一种比较常用的优化方法是,我们将 freq根据数从小到大排序,这样我们在递归时会先选择小的数,再选择大的数。这样做的好处是,当我们递归到 dfs(pos,rest)
时,如果freq[pos][0]
已经大于 rest,那么后面还没有递归到的数也都大于 rest,这就说明不可能再选择若干个和为 rest的数放入列表了。此时,我们就可以直接回溯。
code
class Solution {
private:
vector<pair<int,int>> freq;
vector<int> sequence;
vector<vector<int>> ret;
public:
void dfs(int pos, int rest) {
if(rest == 0) {
ret.emplace_back(sequence);
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) {
sequence.emplace_back(freq[pos].first);
dfs(pos + 1, rest - i * freq[pos].first);
}
for (int i = 1; i <= most; ++i) {
sequence.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
std::sort(candidates.begin(), candidates.end());
for(auto &num: candidates) {
if(freq.empty() || num != freq.back().first) {
freq.emplace_back(num, 1);
} else {
++freq.back().second;
}
}
dfs(0, target);
return ret;
}
};
最核心的还是对于重复的数字只使用1次这个思路,就是首先排序,然后分析该数字能够使用的最多的次数,不仅要判断 rest 中最多包含几个重复数字,也要判断重复数字的个数。因此首先就得对candidate数组进行排序。
int most = min(rest / freq[pos].first, freq[pos].second);
for (int i = 1; i <= most; ++i) {
sequence.emplace_back(freq[pos].first);
dfs(pos + 1, rest - i * freq[pos].first);
}
for (int i = 1; i <= most; ++i) {
sequence.pop_back();
}
Reference
- 组合总和 II
- 力扣官方题解