2023-02-01 10:09:19 +00:00
|
|
|
|
# 组合问题
|
|
|
|
|
|
2023-02-03 10:07:50 +00:00
|
|
|
|
组合问题:N 个数里面按一定规则找出 k 个数的集合
|
|
|
|
|
|
2023-02-01 10:09:19 +00:00
|
|
|
|
## [77. 组合](https://leetcode.cn/problems/combinations/description/)
|
|
|
|
|
|
|
|
|
|
![combinations](https://paste.sainnhe.dev/Cytj.png)
|
|
|
|
|
|
|
|
|
|
每个节点存储的数据是什么?是一个 `vector<int>` 类型的数据,代表当前节点的路径。
|
|
|
|
|
|
|
|
|
|
下一个节点的路径需要基于上一个节点的路径来获得,因此传入参数应该有一个 `vector<int> path`。另外,还需要有一个 `vector<vector<int>> &result` 用来存放结果。
|
|
|
|
|
|
|
|
|
|
终止条件是什么?回溯法中的每个节点并不是真的树状节点,没有 `nullptr` ,因此用空指针来判断是否到了叶子节点并不合理。
|
|
|
|
|
|
|
|
|
|
本题中我们可以通过高度来判断是否达到了叶子节点,如果 `path.size() == k` 则说明到达了叶子节点,则停止迭代,并把当前路径添加到结果变量中。
|
|
|
|
|
|
|
|
|
|
因此我们还需要高度 `k`,`int k` 也应该是一个传入参数。
|
|
|
|
|
|
|
|
|
|
为了防止重复,我们需要在 `[1, n]` 中的一个子区间 `[begin, n]` 中选择一个数,`[1, begin]` 是我们已经选过了的,因此我们需要 `int n` 和 `int begin` 来作为传入参数。
|
|
|
|
|
|
|
|
|
|
在每次迭代中,我们从 `[begin, n]` 中挨个选一个数加到上一轮迭代传递进来的 `path` 中,然后进行下一轮迭代。
|
2023-02-02 01:41:56 +00:00
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
void combineDFS(int n, int k, int begin, vector<int> &path,
|
|
|
|
|
vector<vector<int>> &result) {
|
|
|
|
|
// 当 path 长度等于 k 时停止迭代,并将加入结果
|
|
|
|
|
if (path.size() == k) {
|
|
|
|
|
result.push_back(path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 遍历可能的搜索起点
|
|
|
|
|
for (int i = begin; i <= n; ++i) {
|
|
|
|
|
// 将 i 加入路径
|
|
|
|
|
path.push_back(i);
|
|
|
|
|
// 下一轮搜索
|
|
|
|
|
combineDFS(n, k, i + 1, path, result);
|
|
|
|
|
// 回溯,撤销处理的节点
|
|
|
|
|
path.pop_back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们现在来看看能不能优化。
|
|
|
|
|
|
|
|
|
|
![optimization](https://paste.sainnhe.dev/NzcF.png)
|
|
|
|
|
|
|
|
|
|
在上图的这种情况中,每一层其实都可以剪掉一些不可能的分支,我们可以对每一层循环的终止条件进行限制,从而剪枝。
|
|
|
|
|
|
|
|
|
|
优化后的代码如下:
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
void combineDFS(int n, int k, int begin, vector<int> &path,
|
|
|
|
|
vector<vector<int>> &result) {
|
|
|
|
|
// 当 path 长度等于 k 时停止迭代,并将加入结果
|
|
|
|
|
if (path.size() == k) {
|
|
|
|
|
result.push_back(path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 遍历可能的搜索起点
|
|
|
|
|
// 在这一步中,每一次循环都可以对末尾进行限制来剪枝
|
|
|
|
|
for (int i = begin; i <= n - (k - path.size()) + 1; ++i) {
|
|
|
|
|
// 将 i 加入路径
|
|
|
|
|
path.push_back(i);
|
|
|
|
|
// 下一轮搜索
|
|
|
|
|
combineDFS(n, k, i + 1, path, result);
|
|
|
|
|
// 回溯,撤销处理的节点
|
|
|
|
|
path.pop_back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
2023-02-02 02:14:39 +00:00
|
|
|
|
|
2023-02-02 03:33:45 +00:00
|
|
|
|
## [216. 组合总和 III](https://leetcode.cn/problems/combination-sum-iii/)
|
|
|
|
|
|
2023-02-02 02:40:18 +00:00
|
|
|
|
## [39. 组合总和](https://leetcode.cn/problems/combination-sum/)
|
2023-02-02 02:14:39 +00:00
|
|
|
|
|
2023-02-02 03:33:45 +00:00
|
|
|
|
## [40. 组合总和 II](https://leetcode.cn/problems/combination-sum-ii/)
|
|
|
|
|
|
|
|
|
|
最难的一个组合总和,因为 `candidates` 有重复元素,而要求最终结果不能重复。
|
|
|
|
|
|
|
|
|
|
e.g. 1
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Input: candidates = [10,1,2,7,6,1,5], target = 8
|
|
|
|
|
Output:
|
|
|
|
|
[
|
|
|
|
|
[1,1,6],
|
|
|
|
|
[1,2,5],
|
|
|
|
|
[1,7],
|
|
|
|
|
[2,6]
|
|
|
|
|
]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
如果你只是单纯地在 s0039 的基础上在下一次递归中将 `startIndex` 设为 `i + 1` 那么最终结果就会出现两个 `[1, 2, 5]`。
|
|
|
|
|
|
|
|
|
|
如果你直接排除 `candidates[i] == candidates[i - 1]` 的情形,那么最终结果就没有 `[1, 1, 6]`。
|
|
|
|
|
|
|
|
|
|
正确的逻辑应该是如果 `candidates[i] == candidates[i - 1]` 且 `candidates[i - 1]` 使用过,则剪枝。
|
|
|
|
|
|
2023-02-03 04:14:39 +00:00
|
|
|
|
![demo](https://paste.sainnhe.dev/DMfz.png)
|
|
|
|
|
|
|
|
|
|
那么我们现在要来定义一下什么叫“使用过”。这张图里面有两种“使用过”,第一种使用过是“在树枝上使用过”,第二种使用过是“在数层上使用过”。
|
|
|
|
|
|
|
|
|
|
第一种“使用过”显然是合法的,我们允许元素在一条树枝上重复出现。而第二种“使用过”是不合法的,生成的结果重复了。
|
|
|
|
|
|
|
|
|
|
因此我们只需要对第二种“使用过”进行剪枝,而保留第一种“使用过”。
|
|
|
|
|
|
|
|
|
|
怎么做呢?我们创建一个 `vector<bool> used` 用来记录元素是否在树枝上出现过,初始化为 `false`。
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
used[i] = true;
|
|
|
|
|
combinationSum2DFS(candidates, target, i + 1, path, sum + candidates[i],
|
|
|
|
|
used, result);
|
|
|
|
|
used[i] = false;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
那么 `used[i - 1] == true` 说明 `candidates[i - 1]` 在树枝上出现过,我们需要保留这种情况,不剪枝。
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
// 剪枝,但保留树枝重复的情况
|
|
|
|
|
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
|
|
|
|
|
continue;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
另外需要注意一点,为了进行剪枝,我们需要对 `candidates` 进行排序:
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
// 对 candidates 进行升序排序,这是为了进行剪枝
|
|
|
|
|
sort(candidates.begin(), candidates.end());
|
|
|
|
|
```
|
2023-02-03 10:20:46 +00:00
|
|
|
|
|
|
|
|
|
完整代码如下:
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
void combinationSum2DFS(vector<int> &candidates, int target, int startIndex,
|
|
|
|
|
vector<int> &path, int sum, vector<bool> &used,
|
|
|
|
|
vector<vector<int>> &result) {
|
|
|
|
|
// 结束条件:总和等于 target 。不存在总和大于 target 的情况,因为已经被剪枝了
|
|
|
|
|
if (sum == target) {
|
|
|
|
|
result.push_back(path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始迭代
|
|
|
|
|
int size = candidates.size();
|
|
|
|
|
for (int i = startIndex; i < size; ++i) {
|
|
|
|
|
// 剪枝,当现在节点的 sum 已经超过了 target,就没必要继续迭代了
|
|
|
|
|
if (sum + candidates[i] > target) break;
|
|
|
|
|
// 剪枝,但保留树枝重复的情况
|
|
|
|
|
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
|
|
|
|
|
continue;
|
|
|
|
|
path.push_back(candidates[i]);
|
|
|
|
|
used[i] = true;
|
|
|
|
|
combinationSum2DFS(candidates, target, i + 1, path, sum + candidates[i],
|
|
|
|
|
used, result);
|
|
|
|
|
used[i] = false;
|
|
|
|
|
path.pop_back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|