回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
将遍历过程抽象为一个树形结构
针对需要使用回溯算法的问题,可以使用回溯三部曲来分析回溯算法
回溯法的模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
文章链接:https://programmercarl.com/0077.%E7%BB%84%E5%90%88.html
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
1. 分析
通常在做这样的题目时,会考虑使用暴力解法,需要几个数就用几层for循环,但是在k值不确定的情况下,无法确定具体要用几层for循环,这时候就需要使用递归来替代多层for循环,即深度方向的循环。
画出树形结构如下:
可以直观的看出其搜索的过程:for循环横向遍历,递归纵向遍历,回溯不断调整结果集
2. 剪枝
优化回溯算法只有剪枝一种方法
树形结构如图:
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
在for循环上做剪枝操作是回溯法剪枝的常见套路!
文章链接:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
- 所有数字都是正整数。
- 解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
1. 分析
本题相比2.1.1组合问题加了一个元素总和的限制
树形结构如图:
整体思路是一样的,只是需要多记录一个和的结果,用于和target做比较
2.剪枝
本题可以进行2个剪枝
文章链接:https://programmercarl.com/0039.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8C.html
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
- 所有数字(包括 target)都是正整数。
- 解集不能包含重复的组合。
示例 1:- 输入:candidates = [2,3,6,7], target = 7,
- 所求解集为: [ [7], [2,2,3] ]
示例 2:- 输入:candidates = [2,3,5], target = 8,
- 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
1. 分析
本题和2.1.2组合总和问题相比,去掉了数字个数的限制,元素可以无限制重复被选取,保留了总和大小限制,所以间接的也是有个数的限制。
虽然没有限制每个元素只能取一次,但是还是需要startindex来控制for循环的起始位置,例如[2,3,5],如果不控制,那么当取3时,还可以取到2,那么就会有[3,3,2]、[3,2,3]这两个组合,这两个组合与[2,3,3]实际上是重复的组合
什么时候需要使用startindex?
文章链接:https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
- 输入: candidates = [10,1,2,7,6,1,5], target = 8,
- 输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:- 输入: candidates = [2,5,2,1,2], target = 5,
- 输出:
[
[1,2,2],
[5]
]
1. 分析
本题集合元素会有重复,但要求解集不能包含重复的组合,这就涉及到去重
去重则要分为树枝去重和树层去重
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。
图中橘黄色标注的used的变化,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
2.剪枝
本题仍然只针对和大于target进行剪枝
文章链接:https://programmercarl.com/0017.%E7%94%B5%E8%AF%9D%E5%8F%B7%E7%A0%81%E7%9A%84%E5%AD%97%E6%AF%8D%E7%BB%84%E5%90%88.html
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
- 输入:“23”
- 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
1. 分析
本题使用了多个集合来求组合,就不需要从startindex开始遍历了
因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合
文章链接:https://programmercarl.com/0131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.html
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”] ]
1. 分析
本题难点在于:
如果能想到用求解组合问题的思路来解决 切割问题本题就成功一大半了
本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1
树形结构如下:
2.剪枝
本题不需要进行剪枝,回文子串的判断是题目本身的要求
文章链接:https://programmercarl.com/0078.%E5%AD%90%E9%9B%86.html
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
1. 分析
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
树形结构如下:
本题其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整棵树。
因为每次递归的下一层就是从i+1开始的,所以不会无限递归。
如果要写终止条件,注意:result.append(path[:]) 要放在终止条件的上面,因为集合本身就是一个子集
2.剪枝
子集问题不需要任何剪枝
文章链接:https://programmercarl.com/0090.%E5%AD%90%E9%9B%86II.html
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
- 输入: [1,2,2]
- 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
1. 分析
本题相比如上一题的子集问题,数组本身是包含重复元素的,但是解集不能包含重复的子集,也就是需要去重
去重方法与**2.1.4 组合总和(三)**操作相同
树形结构如下:
文章链接:https://programmercarl.com/0491.%E9%80%92%E5%A2%9E%E5%AD%90%E5%BA%8F%E5%88%97.html
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
- 输入: [4, 6, 7, 7]
- 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
- 给定数组的长度不会超过15。
- 数组中的整数范围是 [-100,100]。
- 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
1. 分析
本题与**2.3.2 子集问题(二)**很类似,都是求子集+包含重复元素
树形结构如下:
但是要注意,去重一定要先对数组进行排序,但是本题要求递增子序列,排序后所有子序列都是递增的了,所以不能排序
用没有排序的集合{2,1,2,2}来举个例子画一个图,如下:
所以本题的去重需要通过哈希表或者数组来去重,本题主要是为了避免树层重复,那么就可以每层都维护一个哈希表,本层元素出现重复就跳过
文章链接:https://programmercarl.com/0046.%E5%85%A8%E6%8E%92%E5%88%97.html
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
- 输入: [1,2,3]
- 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
1. 分析
排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
树形结构如下:
排列问题与组合等问题的不同点在于:
文章链接:https://programmercarl.com/0047.%E5%85%A8%E6%8E%92%E5%88%97II.html
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
- 输入:nums = [1,1,2]
- 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
1. 分析
本题与**2.4.1 排列问题(一)相比,只有原数组存在重复元素这一定不同,这就涉及到去重,去重逻辑与2.1.4 组合总和(三)及2.3.2 子集问题(二)**相同
树形结构如下:
1. 使用set去重
上面的题目都是统一使用used数组来去重的,其实使用set也可以用来去重!
关键点在于set放的位置
2. 使用startindex去重
是否使用used数组来去重,取决于下一次递归时,是否从0开始,如果递归的时候下一个startIndex是i+1而不是0,那么就不需要使用used数组,
使用set去重的版本相对于used数组的版本效率都要低很多: