提示:努力生活,开心、快乐的一天
文章目录
回溯法理论基础
回溯是递归的副产品,只要有递归就会有回溯
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下
回溯法的模板:
function backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合问题
组合问题
for循环横向遍历,递归纵向遍历,回溯不断调整结果集
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
组合总和
组合总和(一)
加了一个元素总和的限制,剪枝操作:已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉,剪枝的代码可以在for循环加上 i <= 9 - (k - path.size()) + 1 的限制
组合总和(二)
本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制
本题剪枝操作:
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
组合总和(三)
集合元素会有重复,但要求解集不能包含重复的组合,添加去重逻辑,“树枝去重”和“树层去重”,去重问题先排序
多个集合求组合
每一个数字代表的是不同集合,也就是求不同集合之间的组合,所以不需要startIndex
切割问题
切割问题难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
用求解组合问题的思路来解决 切割问题,后序如何模拟切割线,如何终止,如何截取子串,最后判断回文
子集问题
子集问题(一)
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果
result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉结果
if (startIndex >= nums.size()) { // 终止条件可以不加
return;
}
子集问题(二)
针对子集问题进行去重,组合中的去重逻辑
递增子序列
不能先进行排序
排列问题
排列问题(一)
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,所以处理排列问题就不用使用startIndex了。
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
排列问题(二)
又一次强调了“树层去重”和“树枝去重”
这道题目神奇的地方就是used[i - 1] == false(树层去重)也可以,used[i - 1] == true(树枝去重)也可以!
树层上去重(used[i - 1] == false),的树形结构如下:
树枝上去重(used[i - 1] == true)的树型结构如下:
使用(used[i - 1] == false),即树层去重,效率更高!
去重问题
统一使用used数组来去重的,其实使用set也可以用来去重!
使用used数组去重 和 使用set去重 两种写法的性能差异:
- 使用set去重的版本相对于used数组的版本效率都要低很多
- 使用set去重,不仅时间复杂度高了,空间复杂度也高了
性能分析
1、子集问题分析:
- 时间复杂度:O(2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2n)
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
2、排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
- 空间复杂度:O(n),和子集问题同理。
3、组合问题分析:
- 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。
🎈今日心得
今天很忙,所以hard题就没做,周末休息日再看吧,只是复习了一下回溯算法