随想录训练营21/60 | 回溯理论基础;LC 77. 组合
回溯理论基础
什么时回溯?
之前学习了二叉树的知识,遍历二叉树时,一般用递归法,在计算和深度高度相关的题时也用到了回溯。回到父节点走其它孩子节点
回溯即递归,有递归就可以有回溯。回溯的本质是穷举,穷举所有可能,不是一个高效的算法。为什么还要学习回溯呢?
为什么回溯?
即使回溯效率低,但有些问题只能通过穷举法解决,如:组合问题;切割问题;子集问题;排列问题;棋盘问题。
如何理解回溯?
回溯法解决的问题都可以抽象为树形结构,以上问题本质都是在集合中找符合规则的子集,集合的大小就是树的宽度,递归的深度就是树的深度。
回溯的模板
与之前学习的三部曲相同:
先确定递归的返回值和参数;回溯一般没有返回值(void),参数比二叉树难,一般是根据递归逻辑确定参数
然后确定递归的终止条件;找到满足条件的自己就保存结果并返回
最后进行单层的递归逻辑;在集合中递归搜索,集合的大小就是孩子的多少。
伪代码:
void backtracking(param){
if(终止条件){
//存放结果;
return;
}
for(选择本层集合中的元素){
处理节点;
backtracking(param);//递归
回溯;//有时候回溯操作不明显,隐含在递归操作中
}
}
LC 77. 组合
题目链接:LC 77. 组合
思路:递归的输入为剩余的集合,当满足终止条件就保存结果。思路不难,不要求顺序,也就是不能重复。
代码:
class Solution {
public:
//递归函数
//set是集合,cur是当前的组合
vector<vector<int>> result;
void backtracking(vector<int> set, vector<int>cur, int k){
//当组合的大小等于要求时,就保存并返回
if(cur.size()==k){
result.push_back(cur);
return;
}
//不满足就进行递归,遍历传入的集合,注意之前处理过的节点之后就不能处理了
for(int i=0; i<set.size();i++){
cur.push_back(set[i]);
vector<int> lastSet(set.begin()+i+1,set.end());
backtracking(lastSet, cur, k);
cur.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
vector<int> set;
for(int i=1; i<=n; ++i){
set.push_back(i);
}
vector<int> cur;
backtracking(set, cur, k);
return result;
}
//--------上面的内存消耗太高,递归可以只传开始的--------
vector<int> path;
vector<vector<int>> result;
void backtracking(int begin, int n, int k){
if(path.size()==k){//当路径长度到达k就可以保存结果
result.push_back(path);
return;
}
//当没有到达指定长度时
for(int i=begin; i<=n; i++){
path.push_back(i);//将begin放到path中,然后进行递归
backtracking(i+1, n, k);
path.pop_back();//回溯
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(1, n, k);
return result;
}
};