1.理论基础
1.1 什么是回溯法
-
回溯搜索法是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。
回溯函数也就是递归函数,指的都是一个函数。
1.2 回溯法的效率
- 回溯法很难,不好理解,但回溯法并不是什么高效的算法
- 回溯的本质是穷举,穷举所有可能,然后选出想要的答案。如果想让回溯法高效,可以加一些剪枝操作,但也改变不了回溯法就是穷举的本质。
- 有些题只能用回溯法穷举,所以选用这种并不高效的方法。
1.3 回溯法解决的问题
-
组合问题:N个数里按一定规则找出k个数的集合
-
切割问题:一个字符串按一定规则有几种切割方式
-
子集问题:一个N个数的集合里有多少符合条件的子集
-
排列问题:N个数按一定规则全排列,有几种排列方式
-
棋盘问题:N皇后,解数独等
组合和排列的区别:组合不强调元素顺序,排列强调元素顺序。
1.4 如何理解回溯法
- 回溯法解决的问题都可以抽象为树形结构!
- 回溯法解决的都是在集合中递归查找子集的问题,集合的大小就构成了树的宽度、递归的深度构成了树的深度。
- 递归就要有终止条件,所以必然是一颗高度有限的数(N叉树)
1.5 回溯法模板(三部曲)
-
返回值以及参数
- 卡尔习惯函数起名为backtracking
- 返回值:一般为void
- 参数:回溯算法需要的参数不像二叉树递归那么容易一次性确定下来,一般是先写逻辑,然后需要什么参数就填什么参数。
- 伪代码:
void backtracking(参数)
-
终止条件
- 一般来说搜到叶子节点,也就找到了满足条件的一条答案,把这个答案存放起来并结束本层递归
- 伪代码:
if(终止条件) { 存放结果; return; }
-
遍历过程
- 回溯法一般在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度。
- 伪代码如下
for(选择:本层集合中元素(树种节点孩子的数量就是结合的大小)) { //横向遍历 处理节点; backtracking(路径, 选择列表);//递归,纵向遍历 回溯,撤销处理结果 }
- for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
- backtracking这里自己调用自己,实现递归
- 回溯法一般在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成树的深度。
-
回溯算法模板框架
void backtracking(参数) {
if(终止条件) {
存放结果;
return;
}
for(选择:本层集合中的元素(树种节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表);
回溯,撤销处理结果;
}
}
77. 组合
-
题意:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
-
思路:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
public void backtracking(int n, int k, int startIndex) {
if(path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for(int i = startIndex; i <= n; i++) {
path.add(i);
backtracking(n, k, i+1);
path.remove(path.size() - 1);
}
}
}