回溯问题总结
个人心得
本博客大部分参考自:代码随想录 - 回溯法总结篇
问题本质
回溯是递归的副产品,常与深度优先搜索、二叉树遍历一起出现。本质是暴力搜索算法(最多剪枝优化),只不过是用递归的方式控制了for循环嵌套的数量,即:for循环横向遍历,递归纵深遍历,回溯不断调整结果集。将遍历过程抽象为树的结构可以更好地理解。
解题步骤
“Carl回溯三部曲”
-
回溯函数backtracking模板的返回值及其参数
-
回溯函数的终止条件
-
回溯搜索的遍历过程
我的一些说明
1、单个集合求解时需要设置startIndex,多个集合求组和不需要
2、回溯部分的backtracking()参数startIndex,不限次数可从i开始,限制次数必须往后i + 1
3、每层遍历时主逻辑这样写:求组合时for当中int i = startIndex,否则求排列时一般从i = 0开始
4、层次去重时,要使用used数组记录元素是否被访问,出于时空复杂度的考量尽量不使用unordered_map去重,注意提前对原数组的排序问题
5、backtracking()函数的返回值一般是void,何时为bool?因为解数独找到一个符合的条件(就在树的叶子节点上)立刻 就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值,这一点在 重新安排行程、回溯算法:N皇后问题 中已经介绍过了,一样的道理。
6、注意返回条件和收集result的位置关系
- 这里粘贴一个录友总结好的框架:
解题范围
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
组合问题
-
77. 组合 - 力扣(LeetCode),给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。组合不考虑排列顺序,因此for从i = startIndex开始遍历,剪枝时,i <= n - (k - path.size()) + 1
// path.size()已经选择的元素个数
// k - path.size()还需选择的元素个数
// 在集合n中 至多 要从该起始位置 : n - (k - path.size()) + 1,开始遍历,因为再往后才开始的话集合中的元素将取不够
-
216. 组合总和 III - 力扣(LeetCode),找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。比第一题多了总和的限制。照旧,从i = startIndex开始遍历,可以有i <= n - (k - path.size()) + 1剪枝和总和超过sum就return两种剪枝
-
39. 组合总和 - 力扣(LeetCode),比第二题少了限制,不限制选取的次数,因此递归backtracking部分不用i + 1,直接i。剪枝只有判断sum一处。最好先对候选数组排序
-
40. 组合总和 II - 力扣(LeetCode),难在去重。这里采用“层次去重”,最好先排序,然后引入uesd数组记录元素是否被访问过
多个集合求组和
- 17. 电话号码的字母组合 - 力扣(LeetCode),注意这里for循环可不是从startIndex开始遍历的,因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合。而且index也不同与以往的startIndex,是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度
切割问题(用求解组合的思路来解决)
有如下几个难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
子集问题
- 78. 子集 - 力扣(LeetCode),注意收集result要放在终止条件的上面
- 90. 子集 II - 力扣(LeetCode),子集去重,有uesd数组法和unordered_set法,考虑到时空复杂度建议用前者。一定要先排序!
- 491. 递增子序列 - 力扣(LeetCode),一定不能排序,否得都将是递增子序列,因此去重的逻辑需要改变
排列问题
- 46. 全排列 - 力扣(LeetCode),收集所有的叶子节点,每层都是从0开始搜索而不是startIndex,需要used数组记录path里都放了哪些元素
- 47. 全排列 II - 力扣(LeetCode),去重版全排列,树层去重要比树枝去重的效率更高,少做了很多无用搜索
重新安排行程
- 332. 重新安排行程 - 力扣(LeetCode),难!难在容器的选择和使用,backtracking函数返回值为bool
棋盘问题
- 51. N 皇后 - 力扣(LeetCode),棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法模板
- 37. 解数独 - 力扣(LeetCode),二维递归
性能分析
子集问题分析:
- 时间复杂度: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),和子集问题同理。
N皇后问题分析:
- 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * … * 1。
- 空间复杂度:O(n),和子集问题同理。
解数独问题分析:
- 时间复杂度:O(9^m) , m是’.'的数目。
皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * … * 1。 - 空间复杂度:O(n),和子集问题同理。
解数独问题分析:
- 时间复杂度:O(9^m) , m是’.'的数目。
- 空间复杂度:O(n2),递归的深度是n2