回溯法总结
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯并不是高效的算法,本质是暴力搜索,但是在一定情形下可以进行剪枝操作
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独
回溯法的模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
在回溯法上最大的收获就是有一部分题可以自己在模板的基础上解答出来,不需要看卡哥的题解了,这是在回溯与递归解题上最大的感受;有一部分题是由于自己想得太复杂了,通过看卡哥题解结合图解的方式可以清楚的了解如何完成;从一开始按照卡哥的回溯三部曲慢慢进行,每一步都在程序中写上注释及详解,到后面可以看到题目有大致思路自己可以照猫画虎的写出来真心有成就感
组合问题
可以直观的看出其搜索的过程:for循环横向遍历,递归纵向遍历,回溯不断调整结果集,
剪枝优化
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了
组合总和(一)
已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉
剪枝的代码可以在for循环加上 i <= 9 - (k - path.size()) + 1
的限制!
组合总和(二)
本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startIndex;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
以上情况是建立在组合的基础上的
本题的剪枝优化,如下:
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
组合总和(三)
集合元素会有重复,但要求解集不能包含重复的组合
可以看出在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
多个集合求组合
在电话号码组合中,开始用多个集合来求组合
切割问题
难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
切割过的地方不能重复切割所以递归函数需要传入i + 1
子集问题
子集问题(一)
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果
本题其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了
子集问题(二)
开始针对子集问题进行去重
递增子序列
可以使用set针对同一父节点本层去重,但子集问题一定要排序
排列问题
排列问题(一)
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次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),即树层去重,效率更高!
去重问题
使用set去重的版本相对于used数组的版本效率都要低很多
而使用used数组在时间复杂度上几乎没有额外负担!
使用set去重,不仅时间复杂度高了,空间复杂度也高了
性能分析
子集问题分析:
- 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
- 空间复杂度:O(n),和子集问题同理。
组合问题分析:
- 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。