回溯算法理论基础
1、回溯法的定义
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。
回溯是递归的副产品,只要有递归就会有回溯;回溯函数也就是递归函数,指的都是一个函数
2、回溯法的效率
回溯的本质是穷举所有可能出现的情况,因此回溯并不是高效算法
3、回溯法解决的问题
回溯法,一般可以解决如下几种问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 棋盘问题:N皇后,解数独
组合是不强调元素顺序的,排列是强调元素顺序
4、如何理解回溯法
回溯法所要解决的问题可以抽象为树形结构;因为回溯法解决的都是在集合中递归查找子集,集合的大小构成了树的宽度,递归的深度构成了树的深度
5、回溯法模板
回溯三部曲:
1、回溯函数模板返回值以及参数--函数起名字为backtracking;回溯算法中函数返回值一般为void;回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数
void backtracking(参数)
2、回溯函数终止条件--什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归
if (终止条件) {
存放结果;
return;
}
3、回溯搜索的遍历过程--回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
77. 组合
采用一般暴力解法就是普通的嵌套循环,但随着k的增大,循环嵌套只会增加时间复杂度
因此本题采用回溯法,n是树的宽度,k是树的宽度
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围
回溯三部曲
1、递归函数的返回值以及参数----函数里参数太多影响可读性,因此定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合;函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数;还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)
2、回溯函数终止条件----path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径
if (path.size() == k) {
result.push_back(path);
return;
}
3、单层搜索的过程----回溯法的搜索过程就是一个树型结构的遍历过程,for循环用来横向遍历,递归的过程是纵向遍历
for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
本题代码如下:
class Solution {
public:
vector<vector<int>> result; //用于存放各种结果集
vector<int> path; //用于记录单次集合
void backtracking(int n, int k, int startInde) {
if (path.size() == k) {
result.push_back(path);
return ;
}
for (int i = startInde; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
result.clear();
path.clear();
backtracking(n, k, 1);
return result;
}
};