77. 组合
分析
如果 k = 2
就直接可以用两层for循环可以解决
int n = 4;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
cout << i << " " << j << endl;
}
}
但如果 k = 10086呢
不可能去用10086层for循环
这时候可以考虑取用回溯算法来解决暴力都解决不了的问题
递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了。
此时,在
n = 100 , k = 50的时候
就是递归50层
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围
对于一个集合,相当于是递归树的宽度,而k相当于是递归的深度
思路
回溯三部曲
- 递归的返回值以及参数
先定义两个全局变量:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
- 回溯函数的三个参数
n, 集合大小. k 组合集合的大小(树的深度)
startIndex 记录下一层递归从哪开始,避免重复
比如1,2,3,4下一层就要从2开始
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)
- 回溯终止条件
path数组的大小等于k时,找到了一个子集大小为k的组合,在path存的就是根节点到叶子结点的路径
if (path.size() == k) {
result.push_back(path);
return;
}
- 单层搜索的过程
for循环, => 横向遍历集合
递归 => 纵向遍历
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
回溯模版
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
复杂度
- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
Code
C++
class Solution {
private:
vector<int> path;
vector<vector<int>> res;
void backtracking(int n, int k, int startIndex){
if(path.size() == k) {
res.push_back(path);
return;
}
for(int i = startIndex; i <= n; i++){
path.push_back(i); // 处理结点
backtracking(n , k , i+1); //递归,不能使用++i改变原来的值
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
};
java
class Solution {
private List<List<Integer>> res = new ArrayList<>();
private LinkedList<Integer> path = new LinkedList<>();
private void backtracking(int n, int k , int startIndex){
if(path.size() == k) {
res.add(new ArrayList<>(path));
return ;
}
for(int i = startIndex; i <= n ; i++){
path.add(i);
backtracking(n, k , i + 1);
path.removeLast();
}
}
public List<List<Integer>> combine(int n, int k) {
backtracking(n , k , 1);
return res;
}
}
剪枝优化
回溯法虽然是暴力搜索,但有时候也是可以剪枝优化的
在遍历中
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
当n = 9, k = 9, 那么 起始从2开始到9也不到9个数,
这个地方就可以剪枝了
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
举一个n = 9
k - 4
就知道上面三步操作是为什么了
for循环优化后
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
Code(剪枝优化)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};