回溯法也可以叫回溯搜索法,它是一种搜索的方式。我们在二叉树中也多次提到了回溯。回溯是递归的副产品,只要使用了递归就会有回溯,我们我们就可以笼统的将回溯函数称为递归函数
1.组合问题:N个数里面按一定规则找出k个数的集合
2.切割问题:一个字符串按一定规则有几种切割方式
3.子集问题:一个N个数的集合里有多少符合条件的子集
4.排列问题:N个数按一定规则全排列,有几种排列方式
5.棋盘问题:N皇后,解数独等
*不要将排列问题,以及组合问题混为一谈,需要记住组合无序,排列有序
回溯法解决的问题都可以抽象为树形结构。
因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,就构成了树的深度。
递归有终止条件,所以必然是一棵高度有限的树(N叉树)。(下文中会有例图)
在二叉树的递归中,我们使用了递归三部曲,回溯三部曲也差不多
1.回溯函数模板返回值以及参数
在回溯算法中,函数返回值一般为void。
因为回溯算法中需要的参数不像二叉树递归的时候那么容易一次性确定下,所以一般是先写逻辑,然后需要什么参数,再填写什么参数
2.回溯函数的终止条件
我们提到回溯算法是树形结构,既然是树形结构,说明一定存在叶子结点,所以终止条件也就是搜索到了叶子结点,也就是找到了满足条件的答案。
3.回溯搜索的遍历过程
我们之前说的回溯法一般是再集合中递归搜索,集合的大小,构成了树的宽度,递归的深度构成了树的深度,具体如图:
回溯函数遍历过程伪代码如下:
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表);//递归
回溯,撤销处理结果
}
for循环就是遍历集合区间,可以理解一个节点有多少孩子,这个for循环就执行多少次。
可以从图中看出for循环可以理解成横向遍历,backtracking(递归)就是纵向遍历,这样就能把这棵树遍历完全。
综上,我们就可以得出回溯算法的模板框架:
void backtracking(参数){
if(终止条件){
存放结果;
return;
}
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表);
回溯,撤销处理结果
}
}
LeetCode77.组合
题目描述:
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
解题思路:
·这题可以直接使用暴力搜索进行求解,但是如果数量级如果太大,使用暴力求解无法求解了
·所以我们就应该使用回溯算法进行求解,我们先将题目抽象成树形结构如图:
可以看出这棵树,一开始集合是1,2,3,4,从左向右取数,取过的数就不再取
第一次取1,集合变为2,3,4,因为k为2,所以只需要再取一个数就可以了,分别取2,3,4,得集合[1,2][1,3][1,4],以此类推。
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。
因为每次搜索到叶子节点,我们就得到一个结果集,只需要将叶子节点的结果收集起来,就饿可以求得n个数中k个数的组合集合。
代码如下:
class Solution {
public:
vector> result;//用于存放符合条件结果的集合
vector path;//用于存放符合条件的结果
void backtracking(int n,int k,int stratIndex){
if(path.size() == k){
result.push_back(path);
return;
}
for(int i = stratIndex;i <= n;i++){
path.push_back(i);//存放节点
backtracking(n,k,i+1);//递归
path.pop_back();//回溯
}
}
vector> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
};
·时间复杂度:O(n*2^n)
·空间复杂度:O(n)
总结:刚开始学习回溯算法,可以先将题目抽象成N叉树结构,再进行剪枝操作,可以直观的看出搜索过程,以及需要的结果。一定要使用回溯三部曲,以及回溯套用模板,可以做到快速理解快速解题。