回溯算法
理论基础
- 什么是回溯?
- 回溯法也叫回溯搜索法,就是一种搜索方法,而且还是暴力搜索!
- 效率问题:由于它属于暴力搜索,本质是穷举,所以它的效率很低,但是有些问题只能穷举,而且普通的暴力算法无法解决,只能使用回溯法。
- 可以解决什么问题?
- 组合问题:N个数里面按一定规则找出k个数的集合,这个结果集是无序的,而排列是有序的,比如1和2,2和1在组合里是算一组数据,在排列里算两组。
- 排列问题
- 切割问题
- 子集问题
- 棋盘问题
- 如何理解回溯算法?
- 把回溯算法理解为抽象的树,一般情况下集合数目n为树的宽度,递归的深度为树的深度,本质还是递归,只是要一层一层的去遍历每个子节点,递归结束条件一般是碰到叶子节点结束。
回溯模版
- 终止条件
if (终止条件) {
存放结果;
return;
}
- 遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果,为下一轮循环元素使用
}
如上代码:for循环相当于横向遍历,递归相当于纵向遍历。
- 另外回溯法,一般情况下回溯函数返回值为void,结果存到全局的results里或者传的参数里
先解一个组合问题
力扣题目链接
题目描述:
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
思路:
- 先说一下要定义的东西,上面已经提到了我们需要定义一个全局的results,存符合条件的结果集,然后还要存一个path用来存储每次遍历时的路径,这个path就是要最后放入results的,而回溯也是靠path实现的。
- 直接套用回溯模版,第一层,分别选取1,2,3,4放入path中,然后递归子节点,这里子节点就要比第一层大一个值开始,比如第一层为1,那么递归时就从2开始,2的递归从3开始,依此类推
- 注意这里是先走完路径1,然后走路径1的递归,再走路径1的递归的递归,然后一步一步回溯,下一轮循环才会走第一层的2,因为递归返回时有回溯步骤,此时路径path已经为空了,所以此种方法不会有重复元素。
代码实现:
vector<int> path;
vector<vector<int> > results;
void backtracking(int n, int k, int start_index) {
if (path.size() == k) {
results.emplace_back(path);
return;
}
for (int i = start_index; i <= n; ++i) {
path.emplace_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return results;
}
- 剪枝优化:
- 这里可以优化一下,就是我们在横向遍历的时候,当path已经有的元素 + 剩下的元素不足以满足k个数量的时候,我们是不需要继续往下递归了。所以就有 所需节点数:
need = k - path.size()
,那么只要满足i < n - need + 1
的时候,才往下遍历,当 i 大于这个值时,剩下的递归就不满足k个了。
vector<int> path;
vector<vector<int> > results;
void backtracking(int n, int k, int start_index) {
if (path.size() == k) {
results.emplace_back(path);
return;
}
int need = k - path.size();//剪枝优化
for (int i = start_index; i <= n - need + 1; ++i) {
path.emplace_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return results;
}