回溯理论基础
定义
回溯方法就是一种暴力搜索的方法,穷举出所有可能的结果,然后找到想要的答案。回溯函数就是递归函数(有递归也就必定会有回溯),回溯一般隐藏在递归调用的下面一句。
常用于解决的问题
1、组合问题:从N个数里面按照一定的规则找到k个数的组合
2、切割问题:一个字符串按照某种规则有几种切割方法
3、子集问题:一个集合中有多少符合条件的子集
4、排列问题:N个数按照规则全排列,有几种排列方式
5、棋盘问题:N皇后、解数独等
如何理解
所有回溯算法解决的问题都可以抽象为树形结构!因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。
算法模板
因为回溯函数就是递归函数,所以解题时仍要用到递归三部曲。
回溯函数的返回值一般为void。回溯函数中的for循环就是树的横向遍历,递归就是树的纵向遍历。
回溯函数模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77.组合
思路:将组合问题抽象成树形结构,然后用回溯法进行搜索,在叶子节点中收集结果。
该题目抽象的树形结构如下(引用自随想录网站):
class Solution {
public List<List<Integer>> result = new ArrayList<>();
public List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
public 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);
}
return;
}
}
回溯法通过剪枝,会有很大的优化空间。这里举一个例子来说明剪枝过程(引用代码随想录网站)。
class Solution {
public List<List<Integer>> result = new ArrayList<>();
public List<Integer> path = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
public void backtracking(int n, int k, int start) { // start是起始位置的下标
if (path.size() == k) {
List<Integer> temp = new ArrayList<>(path);
result.add(temp);
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);
}
return;
}
}