leetcode_回溯专题篇
回溯
](https://i-blog.csdnimg.cn/blog_migrate/1a85ac519e44dac6ac41b7496a9e661e.jpeg)
回溯问题都可以看成是树的递归;
组合类问题,备选成员元素的数量是递归树的宽度;题目中限制结果的条件是递归树的深度;
递归公式:
E res;
void backtracking(parameters, int startIndex) {
if(终止条件) {
收集结果(res);
return;
}
for(int i = startIndex;..;..) {
处理节点;
backtracking(i + 1);//这里注意是i+1!!!
回溯撤销;
}
return;
}
或类似树的先序遍历:
E backtracking(parameter) {
if(end condition) {
return ...;
}
//可能有判断条件
if(...) {
//处理节点
//递归,进入到第一个分治最后一层
backtracking(...);
//回溯,从末端逐渐往前解决子问题(分支问题);
backtracking(...);
}
return 结果;
}
77. 组合(中等)

这里要求元素是唯一的

解: 组合问题,在递归函数中使用一层循环即可。
class Solution {
//结果数组
List<List<Integer>> res = new ArrayList<>();
//遍历路径
List<Integer> path = new ArrayList<>();
//主函数
public List<List<Integer>> combine(int n, int k) {
backtracking(n, k, 1);
return res;
}
//按照回溯法模板函数
void backtracking(int n, int k, int startIndex) {
//终止条件
if(path.size() == k) {
//注意这里add的是新建的成员path,不然加入的元素最后会被清空
res.add(new ArrayList(path));
return;
}
//单程搜索逻辑
for(int i = startIndex; i <= n; i++) {
//处理节点
path.add(i);
//递归函数
backtracking(n, k, i+1);
//回溯操作,撤销
path.remove(path.size() - 1);
}
}
}
216.组合总和III

这道题给定的备选元素为[1, 9],给定的条件是k个相加和n的组合;
根据三部曲:
-
回溯函数的参数
void backtracking(int sum, int n, int k, int startIndex)
根据题目,参数里设定了当前遍历总和,题目要求的n、k,还有遍历起点startIndex;
结果集合和当前遍历集合设定为全局变量(也可设定在回溯函数参数值中); -
回溯的终止条件
终止条件为遍历之和为n并且当前遍历集合元素为k个数,将当前遍历集合加入结果集并返回;其他情况直接返回; -
单层搜索过程
首先处理节点,遍历到的节点加入当前遍历集合,并且加入到当前遍历之和;
之后进行回溯,因为组合中不存在重复数字,回溯函数传参时i+1;
最后撤销操作,将当前遍历之和去掉当前元素值,当前遍历集合去掉当前元素;
class Solution {
List<Integer> path = new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(1, k, n, 0);
return res;
}
public void backtracking(int startIndex, int k, int n, int sum) {
if(path.size() == k) {
if(sum == n) {
res.add(new ArrayList<>(path));
}
return;
}
for(int i = startIndex; i <= 9; i++) {
path.add(i);
sum += i;
backtracking(i + 1, k, n, sum);
sum -= i;
path.remove(path.size() - 1);
}
}
}
39.组合总和

候选元素组为candidates,同一个元素可以重复选取(正整数不包含0)
根据三部曲:
-
回溯函数的参数
void backtracking(int sum, int startIndex, int target, int[] candidates)
根据题目,参数里设定了当前遍历总和,题目要求的总和target,还有遍历起点startIndex,与候选元素数组candidates;
结果集合和当前遍历集合设定为全局变量(也可设定在回溯函数参数值中); -
回溯的终止条件
终止条件为遍历之和为target,将当前遍历集合加入结果集并返回;其他情况直接返回; -
单层搜索过程
首先处理节点,遍历到的节点加入当前遍历集合,并且加入到当前遍历之和;
之后进行回溯,因为组合中不存在重复数字,回溯函数传参时i+1;
最后撤销操作,将当前遍历之和去掉当前元素值,当前遍历集合去掉当前元素;
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> son = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(0, target, 0, candidates);
return res;
}
public void backtracking(int sum, int target, int startIndex, int[] candidates) {
if(sum == target) {
res.add(son);
return;
}
if(sum > target) {
return;
}
for(int i = startIndex; i < candidates.length; i++) {
sum += candidates[i];
son.add(candidates[i]);
// 重点:传递startIndex参数时,将当前指向元素i传入,以便重复进行元素的选取。
backtracking(sum, target, i, candidates);
// 回溯
sum -= candidates[i];
son.remove(son.size() - 1);
}
}
}
40.组合总和II

与组合总和I不同在于,这里给予的元素只能进行一次选择。给予的元素里可能有重复,但是每个元素仅能使用一次。所以这道题目的去重操作是重点。
需要去重的题目中,一般要对其进行元素的排序,方便其进行去重的筛选,比如三数之和。
其实元素仅能使用一次是很好书写的,只需要在回溯函数中在传递startIndex时,将当前指针的下一位i + 1传入即可。比较令人头疼的是去重的判断,如果我们只是在循环中书写为:
if(i > 0 && candidates[i] == candidates[i - 1]) {
continue;
}
就会发生漏掉满足条件的题解。

可能有人会问,为什么同样的去重条件在三数之和中就可以成功进行,在这里就会漏解呢?
这是因为三数之和中,是通过左边界指针进行的去重判断,而这里传输的指针i可能为第2、3、4…位元素的选取指针。这么说可能有点绕,在三数之和中,举例子我们要找和为6的组合,[2, 2, 3…],当我们第一次选取指向第一个2时寻找到了他的解集;[2, 2, 3…]当我们第一次选取指向第二个2时,这时就无需再进行递归选取了,因为他的解集都在一种情况的解集之中,可以直接进行去重。在这道题中,我们当前的i并不一定是第一次进行选取值,也就是说连续能够选取两次2值的情况被意外排除掉了。
正确的去重操作:
1.借助访问数组boolean[] vistied
当前元素值重复出现过,若visited[i - 1] = true,说明i - 1元素被选取,但并非当前值第一次选取的元素,所以应该继续选取。
当前元素值重复出现过,若visited[i - 1] = false,当前元素值重复出现过,但为当前的第i位才为当前值第一次选取的元素,说明解集已经计算出过,当前可以直接跳过(去重)。
// 去重
if(i > 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) {
continue;
}
完整:
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> res = new ArrayList<>();
List<Integer> sub = new ArrayList<>();
boolean[] visited = new boolean[candidates.length];
// 排序
Arrays.sort(candidates);
backtracking(candidates, target, res, sub, 0, 0, visited);
return res;
}
void backtracking(int[] candidates, int target, List<List<Integer>> res, List<Integer> sub, int startIndex, int cur, boolean[] visited) {
if(cur == target) {
res.add(new ArrayList<>(sub));
return;
}
for(int i = startIndex; i < candidates.length; i++) {
// 去重
if(i > 0 && candidates[i] == candidates[i - 1] && !visited[i - 1]) {
continue;
}
// 提前阻断,若当前元素作和已经溢出,那么后续一定溢出
if(cur + candidates[i] > target) {
break;
}
visited[i] = true;
sub.add(candidates[i]);
backtracking(candidates, target, res, sub, i + 1, cur + candidates[i], visited);
sub.remove(sub.size() - 1);
visited[i] = false;
}
}
2.更简练的写法,在判断中判断当前指针元素i是否为首次该元素值的选择startIndex
startIndex一定是在该轮次选择中该元素值的第一次选择,一定不能漏掉。但是当i > startIndex后,i位元素就有可能是该轮次选择中该值非第一次进行选择了,需要进行去重判断。
// 去重
if(i > startIndex && candidates[i] == candidates[i - 1] ) {
continue;
}
14.2 子集(中等)

这个题目比较特殊,因为求的是子集,path结果需要的不是叶子节点而是每一个叶节点,所以回溯没有终止条件,替代的是直接将当前路由的path数组加入结果集里;

解:递归套一层循环即可。
class Solution {
List<List<Integer>> res;
List<Integer> path;
//主函数
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList<>();
path = new ArrayList<>();
backtracking(nums, 0);
return res;
}
//回溯函数
private void backtracking(int[] nums, int startIndex) {
//没有终止条件,替代的是每一个叶节点都加入结果集
res.add(new ArrayList(path));
//单层搜索逻辑
for(int i = startIndex; i < nums.length; i++) {
//处理节点
path.add(nums[i]);
//递归函数
backtracking(nums, i + 1);
//回溯操作
path.remove(path.size() - 1);
}
}
}
解2:新发现的写法,递归函数中使用两次递归,配合坐标 i完成子集收集。
private static void backtracking2(String s, String c, int i) {
// 当i计数个数满足时,加入子集。
if(i == s.length()) {
if(c.length() > 0) {
System.out.print(c + " ");
}
return;
}
// 使用当前节点,并继续
backtracking2(s, c + s.charAt(i), i + 1);
// 不使用当前节点,同样也使i计数
backtracking2(s, c, i + 1);
}
90.子集II

题解:这道题目和上一道题目多了一个要求,就是元素中可以有重复元素,需要进行去重操作。还是使用回溯法公式,整体结构格式不变。
终止条件:因为需要将每一个叶节点加入结果集,所以没有结束条件判断,每一次回溯都要将当前的路径加入至结果集;
处理节点:处理节点这部分不太一样,因为我们要进行去重操作,在这里我们要排除重复出现的子集,我们需要新增一个boolean数组visited[n]记录数组中每一个元素是否被访问,而这里的判断和普通的判断还不一样(并不是前一个元素和后一个元素相同且前一个元素被访问过就是重复的),在的树形结构里使用过是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层使用过;同一树枝使用过并不是重复,比如[1, 2, 2],而同一树层使用过才是重复,比如[1, 2], [1, 2];如何进行同一层的去重呢,当我们发现单层搜索逻辑中前一个元素等于后一个元素时,如果前一个元数没有访问过,那么就是重复的了。这里为什么是没有访问过呢,因为我们处理节点路径加入元素之后,会在visited[]中设定成true(访问过),递归后进行回溯操作时再将visited[]中设定为false(未访问过)复原;就是说单层搜索逻辑中,同树层前一个元素回溯完会处于为访问的状态,所以未访问过才是判断条件:i > 0 && nums[i] == nums[i-1] && !visited[i-1];
class Solution {
private List<List<Integer>> res;
private List<Integer> path;
private boolean[] visited;
public List<List<Integer>> subsetsWithDup(int[] nums) {
res = new ArrayList<>();
path = new ArrayList<>();
visited = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int startIndex) {
//终止条件
res.add(new ArrayList(path));
//单层搜索逻辑
for(int i = startIndex; i < nums.length; i++) {
//处理节点
if(i > 0 && nums[i] == nums[i-1] && !visited[i-1]) {
continue;
}
path.add(nums[i]);
visited[i] = true;
//递归
backtracking(nums, i + 1);
//回溯
path.remove(path.size() - 1);
visited[i] = false;
}
}
}
// @lc code=end
15.路径总和(简单)


摘自代码随想录:
很多同学会疑惑,递归函数什么时候要有返回值,什么时候没有返回值:
1.先确定递归函数的参数和返回类型
参数:需要二叉树的根节点,需要一个计数器计算是否有路径之和为目标和;
返回类型:
- 如果需要搜索整棵二叉树,且不用处理递归返回值,递归函数不需要返回值;
- 如果需要搜索整棵二叉树,且需要处理递归返回值,递归函数就需要返回值;
- 如果要搜索其中一条符合条件的路径,遇到符合条件的路径就及时返回,那么递归一定需要返回值;
本题是第三种情况:
- 未看题解时,想到了一种方法,通过一次先序遍历完整的遍历一次,如果有满足的路径就将结果
res置为true。但是这种方法有一点劣势,就是要遍历整颗二叉树,而题目的要求实际上可以遇到满足的路径就直接返回true。
class Solution {
boolean res = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
preorder(root, targetSum, 0);
return res;
}
void preorder(TreeNode root , int targetSum, int curSum) {
if(root == null) {
return;
}
//处理节点
curSum += root.val;
if(root.left == null && root.right == null && curSum == targetSum) {
res = true;
}
//递归
preorder(root.left, targetSum, curSum);
//开始回溯,从末节点逐渐往上处理子问题(右节点里是否有条件)
preorder(root.right, targetSum, curSum);
}
}
- 第二种方法就是搜索到满足条件的路径就理解返回结果:
public boolean hasPathSum(TreeNode root, int targetSum) {
//这个是为了排除传入二叉树为空的情况和有单侧子树的节点的情况
if(root == null) {
return false;
}
//遇到叶子节点进行判断当前路径是否满足条件,
if(root.left == null && root.right == null) {
return root.val == targetSum;
}
//这里的递归传参,为了更加方便,传入的目标和为总和减去当前遍历到的节点值;当检测到满足条件的路径后直接就返回true
//若左子树有满足条件的路径返回true
if(hasPathSum(root.left, targetSum - root.val)) return true;
//若右子树有满足条件的路径返回true
if(hasPathSum(root.right, targetSum - root.val)) return true;
//若没有满足条件,返回false
return false;
}
这里回溯条件是自然完成的,遍历到当前节点的和,在传入递归中时才进行处理,递归结束返回到当前节点还是目前需要的节点的和;
这里最后的递归和触底返回false可以再精简一下:
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root == null) {
return false;
}
if(root.left == null && root.right == null) {
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
}
138.复制带随即指针的链表


本题的回溯与之前的公式略有不同,但是思想是相同的。
回溯程序首先是终止条件。
先是处理节点,之后进入递归。
递归到最后一层开始回溯。
返回结果。
class Solution {
/**
* 回溯+哈希表
* T(n) = O(n), S(n) = O(n);
*
**/
// 哈希表的作用是,记录原节点与深拷贝的节点的对应关系
Map<Node, Node> map = new HashMap<>();
public Node copyRandomList(Node head) {
// 终止条件,传入节点为空时,直接返回空
if(head == null) {
return null;
}
// 递归进入条件,哈希表中是否有传入节点
if(!map.containsKey(head)) {
// 若没有则当前节点的拷贝节点还没有进行建立,建立拷贝节点
Node copyHead = new Node(head,val);
// 原节点与拷贝节点放入哈希表对应
map.put(head, copyHead);
// 拷贝节点的next指针指向递归函数(传入原节点的next指针)
copyHead.next = copyRandomList(head.next);
// 回溯开始:
// 拷贝节点的random指针指向递归函数(传入原节点的random指针)
// 第一次执行到这里,就是递归到最后一个节点,并且所有的节点的拷贝节点已经建立过一遍了。
// 递归函数传入的head.random节点,就会返回head.random的拷贝节点
copyHead.random = copyRandomList(head.random);
}
// 最后返回拷贝节点,传入的什么节点,就返回该节点的拷贝节点
return map.get(head);
}
}
22.括号生成

这里通过引入了两个单独的变量left, right分别去控制(,)的生成。
这里没有通过for循环同一个循环里进行递加,而是通过递归进行每一次的括号新增。
class Solution {
public List<String> generateParenthesis(int n) {
StringBuffer sb = new StringBuffer();
List<String> res = new ArrayList<>();
backtracking(n, 0, 0, sb, res);
return res;
}
// 回溯函数,left为左括号数量,right为右括号数量,sb为当前字符串
// 函数里有两层递归,先递归 "(",再递归 ")"。所以会先得到"((()))",再逐步减少左括号增加为右括号"(()())"
private void backtracking(int n, int left, int right, StringBuffer sb, List<String> res) {
// 终止条件,左括号数量和右括号数量都到达n,满足条件记录并返回。
if(left == n && right == n) {
res.add(sb.toString());
return;
}
// 第一层递归,若左括号数量小于n,则添加一个"("
if(left < n) {
sb.append("(");
backtracking(n, left + 1, right, sb, res);
// 回溯
sb.deleteCharAt(sb.length() - 1);
}
// 第二层递归,当右括号数量小于左括号数量时,添加一个 ")"
if(right < left) {
sb.append(")");
backtracking(n, left, right + 1, sb, res);
// 回溯
sb.deleteCharAt(sb.length() - 1);
}
}
}

1221

被折叠的 条评论
为什么被折叠?



