文章目录
- day24学习内容
- 一、修剪二叉搜索树
- 1.1、什么是回溯法
- 1.2、递归与回溯
- 1.3、回溯法的效率
- 1.4、回溯法解决的问题类型
- 1.5、如何理解回溯法
- 1.6、回溯算法模板
- 二、组合问题
- 2.1、思路
- 2.2、正确写法-没有剪枝
- 2.2.1、为什么不能写i < n
- 2.2.2、为什么不能写startIndex==0
- 2.2.3、为什么不能写backtracking(n, k, startIndex + 1);
- 2.3、剪枝优化
- 2.3.1、什么是剪枝
- 2.3.2、剪枝优化的代码
- 2.3.3、为什么是i <= n - (k - path.size()) + 1
- 总结
- 1.感想
- 2.思维导图
day24学习内容
day24主要内容
- 回溯理论基础
- 组合问题
声明
本文思路和文字,引用自《代码随想录》
一、修剪二叉搜索树
1.1、什么是回溯法
基本概念:回溯法也称作回溯搜索法,是一种穷举搜索方式。在解决问题过程中,回溯法会试图分步去解决一个问题。每一步都基于当前的解尝试进一步的解决,如果发现当前的步骤不能得到有效的解或者达到了问题的边界,它将退回一步,尝试其他可能的路径。
1.2、递归与回溯
递归是回溯的基础,几乎所有的回溯问题都可以通过递归函数来实现。回溯法实际上是递归的一个副产品,它利用递归来探索并尝试不同的解决方案。
个人理解,回溯就是递归里面套了for循环
1.3、回溯法的效率
性能说明:虽然回溯法在理解上可能较为复杂且看似不易掌握,但它并不是一个高效的算法。其本质是穷举所有可能的解,然后从中选择满足条件的答案。为了提高效率,可以通过剪枝来减少搜索的空间,但这并不能改变其穷举的本质。
为何使用回溯法:对于某些问题,暴力搜索可能是唯一可行的解决方案。在没有更高效的算法可用时,即使是回溯法,也只能尽力而为,通过剪枝等手段来提高搜索效率。
1.4、回溯法解决的问题类型
- 组合问题:在N个数中按一定规则找出k个数的集合。
不强调顺序
- 切割问题:按一定规则切割一个字符串的不同方式。
- 子集问题:在一个N个数的集合中找出满足条件的子集。
- 排列问题:N个数按一定规则全排列的不同方式。
强调顺序
- 棋盘问题:如N皇后问题,解数独等。
1.5、如何理解回溯法
树形结构的抽象:回溯法解决的问题可以被抽象为树形结构。每一次的递归都代表着向下探索树的一个分支,而每一次回溯则是返回到上一层。整个解空间形成了一棵树,其中的每个节点代表了解决问题的一个步骤。
1.6、回溯算法模板
回溯算法模板:详见卡尔模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
二、组合问题
77.原题链接
2.1、思路
- 和递归很像,可以想象转换成树,在数中进行遍历,只不过在树的字节点里面,有多个数字
2.2、正确写法-没有剪枝
class Solution {
List<List<Integer>> result = new ArrayList();
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 startIndex) {
// 回溯终止条件
if (path.size() == k) {
result.add(new ArrayList(path));
return;
}
// 在单层递归里面for循环
for (int i = startIndex; i <= n; i++) {
// 处理节点
path.add(i);
// 继续递归,注意这里使用i + 1而不是startIndex + 1
backtracking(n, k, i + 1);
// 回溯
path.remove(path.size() - 1);
}
}
}
2.2.1、为什么不能写i < n
- 根据题意,找出从 1 到 n 的所有可能的 k 个数的组合,并且在路径长度等于k时将其添加到结果列表中。
- 举个例子, combine(4,2),其中 n=4 和 k=2,
- 意味着我们需要找到所有可能的从 1 到 4 中挑选 2 个数的组合。
- 如果使用 i <= n,循环将遍历 1, 2, 3, 和 4,确保所有可能的组合都被考虑,包括包含 4 的组合(如 [3, 4])。
- 如果使用 i < n,循环将只遍历 1, 2, 和 3,这意味着任何包含 4 的组合都将被错误地排除在外,导致算法不完整,无法生成所有正确的组合。
2.2.2、为什么不能写startIndex==0
- 看题目啊大哥,写的这么清楚
2.2.3、为什么不能写backtracking(n, k, startIndex + 1);
画个树,很好理解的。
在一层循环的入参,一定是当前选择的元素+1
2.3、剪枝优化
2.3.1、什么是剪枝
就是排除已经考虑过的元素
2.3.2、剪枝优化的代码
class Solution {
List<List<Integer>> result = new ArrayList();
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 startIndex) {
// 回溯终止条件
if (path.size() == k) {
result.add(new ArrayList(path));
return;
}
// 在单层递归里面for循环
for (int i = startIndex; i <= n - k + path.size()+1; i++) {
// 处理节点
path.add(i);
// 继续递归,注意这里使用i + 1而不是startIndex + 1
backtracking(n, k, i + 1);
// 回溯
path.remove(path.size() - 1);
}
}
}
2.3.3、为什么是i <= n - (k - path.size()) + 1
先说结论
:如果剩余可选的元素数量加上当前已选择的元素数量小于所需的元素数量 k,那么就没有必要继续搜索了,因为即使选择了所有剩余的元素,也无法满足组合中应有的元素数量。
推论,为什么是这样?
- n 是总的元素数量。
- k 是需要选择的元素数量。
- path.size() 是当前已经选择的元素数量。
我们需要的是,从当前位置 i
开始,至少还有 k - path.size()
个元素可供选择,以确保能够找到足够的元素组成一个有效的组合。即列表中剩余的元素要大于等于剩余需要选择的元素
。
- n - i + 1 是从当前位置
i
开始,包括i
在内,到结束一共有多少个元素可供选择。
为了保证至少还有 k - path.size()
个元素可选,我们需要有:
n - i + 1 >= k - path.size()
这个不等式简化后得到:
i <= n - k + path.size()
这就是剪枝条件 i <= n - (k - path.size()) + 1
的来源。
总结
1.感想
- 回溯的第一天,剪枝的条件想了好久才想明白、。。
2.思维导图
本文思路引用自代码随想录,感谢代码随想录作者。