递归-回溯
本文参考自代码随想录视频:
https://www.bilibili.com/video/BV1cy4y167mM
https://www.bilibili.com/video/BV1ti4y1L7cv
递归+回溯理论基础
-
只要有递归,就会有回溯,递归函数的下面的部分通常就是回溯的逻辑。
-
回溯是纯暴力的搜索,有时可以通过剪枝做一些优化。
回溯搜索解决的常见问题
- 组合问题
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题
如何理解回溯搜索
所有的回溯法都可以抽象为一个树形结构(n叉树)
回溯法模版
- 回溯函数一般是无返回值的递归函数
backtracking
,其参数通常较多,我们可以在写逻辑的时候根据需要来添加参数 - 终止条件:在回溯递归函数中,到了终止条件时,通常就是我们收集结果的时候了
- 单层搜索的逻辑:通常是一个 for-loop,处理集合的每一个元素,在循环体中处理节点,再进行递归调用,然后再撤销掉处理节点的操作(即所谓回溯)。
以下用伪代码的形式将上述模板写出来:
void backtracking(...) {if (终止条件) {收集结果;return;}for (集合中的元素) {处理节点;backtracking(...); // 递归调用撤销处理节点的操作; // 回溯}
}
LeeCode相关习题
77. 组合
如果我们未曾接触过回溯算法,遇到本题的暴力做法会是怎样做呢,很容易想到,就是嵌套 k 层 for-loop,当 k 是 2 的时候,这也是可行的,但是当 k 是 50 的时候呢,我们发现,即使想要暴力做,程序也不好写了。
这时就考虑到我们的递归-回溯算法,递归中的每一层其实就是一层 for 循环,这样我们就可以自动地去嵌套。
class Solution {
private:void backtracking(vector<vector<int>>& res, vector<int>& curr, int pos, int n, int k) {if (curr.size() == k) {res.push_back(curr);return;}// for (int i=pos; i<n-(k-curr.size()+1; ++i)for (int i=pos; i<=n; ++i) {curr.push_back(i);backtracking(res, curr, i+1, n, k);curr.pop_back();}}
public:vector<vector<int>> combine(int n, int k) {vector<vector<int>> res;vector<int> curr;backtracking(res, curr, 1, n, k);return res;}
}
pos表示本次递归调用起始的位置,res存放最终的全部组合结果,curr存放当前正在搜索的组合结果。
剪枝:注意到当 i 小于 n-(k-curr.size()) + 1 时,当前就已经不可能得到长度为 k 的结果了,故可以调整 for 循环的范围,实现剪枝。代码中注释掉的 for 循环实际上就是剪枝的版本。注意:递归回溯算法在很多时候都是通过合理地减小 for 循环的范围来实现剪枝的。
另一种角度
另一种角度:每遍历到一个元素,我们可以选择将它加入结果或跳过它。
78. 子集
class Solution {
private:void backtracking(vector<vector<int>>& res, const vector<int>& nums, vector<int>& curr, int pos) {if (pos == nums.size()) {res.push_back(curr);return;}// 跳过当前元素backtracking(res, nums, curr, pos+1);// 添加当前元素curr.push_back(nums[pos]);backtracking(res, nums, curr, pos+1);curr.pop_back();}
public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> res;vector<int> curr;backtracking(res, nums, curr, 0);return res;}
};
Ref:
https://www.bilibili.com/video/BV1cy4y167mM
https://www.bilibili.com/video/BV1ti4y1L7cv