回溯理论基础
定义
回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。回溯函数就是递归函数(有递归也就必定会有回溯),回溯一般隐藏在递归调用的下面一句。回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案。
常用于解决的问题
1、组合问题:从N个数里面按照一定的规则找到k个数的组合
2、切割问题:一个字符串按照某种规则有几种切割方法
3、子集问题:一个集合中有多少符合条件的子集
4、排列问题:N个数按照规则全排列,有几种排列方式
5、棋盘问题:N皇后、解数独等
如何理解
所有回溯算法解决的问题都可以抽象为树形结构!因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
算法模板
因为回溯函数就是递归函数,所以解题时仍要用到递归三部曲。
回溯函数的返回值一般为void。回溯函数中的for循环就是树的横向遍历,递归就是树的纵向遍历。
回溯函数模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77.组合
思路:将组合问题抽象成树形结构,然后用回溯法进行搜索,在叶子节点中收集结果。
把组合问题抽象为如下树形结构:
可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
private void backtracking(int n, int k, int start) {
// start是起始位置的下标
if (path.size() == k) {
List<Integer> temp = new ArrayList<>(path);
result.add(temp);
return;
}
for (int i = start; i <= n; i++) {
path.add(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.remove(path.size() - 1); // 回溯,撤销处理的节点
}
}
}
回溯法通过剪枝,会有很大的优化空间。这里举一个例子来说明剪枝过程,如图所示:
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。如果for循环选择的起始位置之后的元素个数已经不足我们需要的元素个数了,那么就没有必要搜索了。
class Solution {
private List<List<Integer>> result = new ArrayList<>();
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
private void backtracking(int n, int k, int start) {
// start是起始位置的下标
if (path.size() == k) {
result.add(new ArrayList<>(path)); // 防止引用传递
return;
}
// 对i的搜索范围进行剪枝,
// 如果从 path.size() 加上 i 以后的数量
// 已经不足以组合成k个,就不用再遍历了
for (int i = start; i <= n - (k - path.size()) + 1; i++) {
path.add(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.remove(path.size() - 1); // 回溯,撤销处理的节点
}
}
}
优化过程如下:
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素个数为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。从2开始搜索都是合理的,可以是组合[2, 3, 4]。