回溯法
理论
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。 ---引用自百度百科
回溯法与递归的关系十分密切,可以说只要有递归就会有回溯
当我们使用回溯法解决问题时,采用的回溯函数就是递归函数
效率
回溯法的性能不是很高,他其实就是一种纯暴力搜索,我们搜索出来所有的可能,然后选出我们需要的结果。
可以解决的问题
- 组合问题
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题
理解回溯法
我们可以把整个回溯法的过程看做对一个多叉树的遍历
回溯法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
例题
以一道力扣题来演示回溯法解题的步骤 [力扣77题](77. 组合 - 力扣(LeetCode) (leetcode-cn.com))
给定两个整数 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]]
根据题条件我们可以构造出这样一棵多叉树:
从这个多叉树来看,每个叶子结点就是我们需要的结果
然后就是代码的实现
class Solution {
//存放一次的结果
LinkedList<Integer> list1 = new LinkedList<>();
//最后需要的结果集
List<List<Integer>> list2 = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
combine1(n,k,1);
return list2;
}
public void combine1(int n,int k,int start){
if(list1.size() == k){
// 当list1中的元素个数为k的时候说明得到了一次结果
list2.add(new ArrayList<>(list1));
return;
}
// 遍历集合
for(int i = start;i <= n;i++){
// 处理节点
list1.add(i);
combine1(n,k,i+1);
// 回溯操作
list1.removeLast();
}
}
}
剪枝操作:
举个例子 当 n = 4 ,k =3时,起点从3开始就无效了,如图
如何实现剪枝操作呢?
for(int i = start;i <= n;i++){
// 处理节点
list1.add(i);
combine1(n,k,i+1);
// 回溯操作
list1.removeLast();
}
可以把上面这部分改成
for(int i = start;i <= n - (k-list1.size())+1;i++){
list1.add(i);
combine1(n,k,i+1);
list1.removeLast();
}
其中
list1.size() 是当前已选择了的节点的个数
k-list1.size() 是仍需选择的节点的个数
n 是该集合的长度
n - (k-list1.size())+1 代表在集合n中至多从起始位置n - (k-list1.size())+1开始遍历
当 list1.size() = 1 时 k-list1.size() = 2 从2开始搜索都是正确的 剪掉 3 4
当 list1.size() = 2 时 k-list1.size() = 1 从3开始搜索都是正确的 剪掉 4
也就是