回溯算法part1
回溯算法part1
理论基础
为什么要用回溯法:
那么既然回溯法并不高效为什么还要用它呢?
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。
回溯法解决的问题:
- 组合问题
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题
如何理解:
回溯法解决的所有问题都可以抽象成树形结构(N叉树),回溯是递归的副产品,所有回溯函数也就是递归函数
时间复杂度,回溯法本身是暴力查找
回溯模板:
感觉跟递归模板类似
- 函数返回值和参数
void backtracking(参数)
- 函数终止条件
if (终止条件) {
存放结果;
return;
}
回溯搜索的遍历过程
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
77. 组合 - 力扣(LeetCode)
不太好理解
在模板中,有这样的图,是回溯的树形结构

那么在本题中,树形结构如下图所示:

本题的回溯模板:
- 参数和返回值:数组范围[1, n]中的n, path返回条件的k,还有确定单层遍历的startIndex
- 返回条件:不要忘记return
if(path.size() == k){
res.add(new ArrayList<>(path));
return;
}
- 单层递归逻辑:其中i的范围是剪枝后的表示,注意递归函数下一层搜索要从i+1开始
for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){ // 控制树的横向遍历
path.add(i); // 处理节点
backtracking(n, k, i+1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.removeLast(); // 回溯,撤销处理的节点
}
剪枝操作
直接看图说话,如果不剪枝,会有很多多余操作

那么如何剪枝?
本题就是指在每一个for循环的开始位置的优化
for(int i = startIndex; i <= n; i++) // 未剪枝前
优化过程我总结不出来,直接copy原话:
接下来看一下优化过程如下:
- 已经选择的元素个数:path.size();
- 还需要的元素个数为: k - path.size();
- 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
这里的+1我认为是因为要从[1, n]集合里找,因为此时的起始位置不是0
完整代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
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 - (k - path.size()) + 1; i++){
path.add(i);
backtracking(n, k, i+1);
path.removeLast();
}
}
}
216. 组合总和 III - 力扣(LeetCode)
感觉这种题目自己画一个图就清楚了,横向是单层的for循环,纵向是递归过程,这点要搞明白
本题与77. 组合 - 力扣(LeetCode)相比,在单层的for循环中,最多进行9次循环,所以,for循环条件注意
for(int i = startIndex; i <= 9; i++)
然后本题的剪枝操作是根据sum来算,如果sum > n(数字和),那么直接return
总体代码如下:
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(k, n, 1);
return res;
}
public void backtracking(int k, int n, int startIndex){
if(sum > n){
return ;
}
if(path.size() == k){
if(sum == n){
res.add(new ArrayList<>(path));
}
return ;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i++){
path.add(i);
sum += i;
backtracking(k, n, i+1);
path.removeLast();
sum -= i;
}
}
}
17. 电话号码的字母组合 - 力扣(LeetCode)
画图:

本题注意数字和字母间的映射关系,用String数组确定
注意回溯三要素的参数中的digitsIndex,这个参数有两个作用,一个确定递归到哪一层了,一个确定返回条件是否成立
代码如下;
class Solution {
List<String> res = new ArrayList<>();
StringBuilder path = new StringBuilder();
String[] nums = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
public List<String> letterCombinations(String digits) {
if(digits.length() == 0){
return res;
}
backtracking(digits, 0);
return res;
}
public void backtracking(String digits, int digitsIndex) {
if(digitsIndex == digits.length()){
res.add(path.toString());
return ;
}
int index = digits.charAt(digitsIndex) - '0';
String num = nums[index];
for(int i = 0; i < num.length(); i++){
char c = num.charAt(i);
path.append(c);
backtracking(digits, digitsIndex + 1);
path.deleteCharAt(path.length() - 1);
}
}
}
心得体会:
没思路、思路混乱,一定要画画图,把图画出来就会清楚很多
浙公网安备 33010602011771号