回溯算法理论基础
代码随想录: 回溯算法理论基础
初印象
- 回溯是递归的副产品,只要有递归就会有回溯。
- 回溯的本质是穷举。
- 所有回溯法的问题都可以抽象为树形结构(N叉树)。
- 递归三要素:确定递归函数的参数和返回值、确定终止条件、确定单层递归的逻辑。
- 回溯三部曲:回溯函数模板返回值以及参数、回溯函数终止条件、回溯搜索的遍历过程。
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
- 回溯,撤销处理结果在代码中如何体现?
看视频讲解后的想法
- 组合、切割、子集、排列、棋盘等问题
- 终止条件语句后,存放完结果不要忘记return,之后用for循环进入单层搜索逻辑,处理节点。虽然代码中是先收集结果再处理节点,但在逻辑中是相反的顺序。
- 回溯撤销处理结果是指:比如在求1、2、3、4的组合问题中,求得1 2组合的结果的情况下,回溯弹出2压入3才能得到1 3组合。
LeetCode77组合
初印象
-
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按任何顺序返回答案。1 <= n <= 20。1 <= k <= n。 -
回溯三要素:参数与返回值,终止条件、遍历过程。参数是两个整数n和k。返回值两个数的组合。终止条件即叶子节点是组合中元素个数已达到k?那么程序终止条件呢?遍历过程可能借助栈。
-
为什么要用回溯算法? for循环,k=2时两层,k=3时三层……n=k=20时非常繁琐无法解决,需要借助回溯法。递归来做层叠嵌套(可以理解是开k层for循环),每一次的递归中嵌套一个for循环。 for循环控制树的横向遍历,递归控制树的纵向遍历。
-
抽象为二叉树,n是树的宽度,k是树的深度。
-
从左往右取,用startindex记录集合从何处开始遍历,防止出现重复的组合。
看视频讲解后的想法
- 程序终止条件通过for循环控制。回溯的终止条件是指比如1234中取2个数的组合,已经取得1得情况下,剩余234,未达到k=2,继续向下递归,取得2形成12组合,此时虽然还剩余34元素未利用,但是已经达到了终止条件k=2,所以终止,向上回溯。
- 由组合的定义可知,1234在首先取得2的情况下,剩下的元素既不能选1也不能选2,只能从34中选,所以需要一个int来记录遍历开始位置。
- for循环中需要一个i=startindex,传入i来递归回溯
- 剪枝
实现过程中遇到的问题
一、原始版本
class Solution {
private:
vector<vector<int>> result;//二维数组存放符合条件的结果的集合
vector<int> path;//存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {//size后面加括号
result.push_back(path);
return;
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);//这里是i+1
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};
- line 3:#include <vector
- line 4: using namespace std
- line10-13,回溯算法嵌套for循环的关键,for循环在树状图中横向,递归回溯在树状图中纵向。
- line13,传入的startIndex是i+1
- line23不要忘记分号
二、剪枝(未更新)
总结
对回溯算法有了一个初步的认识,重点是回溯三要素和回溯算法的模板。终止条件很重要,写不好就死循环。回溯算法能够抽象成n叉树的观点、回溯算法和for循环互相嵌套的观点很新颖,代码有不断复习的必要。