回溯大体分为:组合、排列、子集、切割、搜索几种类型
类型 | 题目链接 | 思路 | |
---|---|---|---|
组合问题 | 77.组合 39.组合总和 40. 组合总和 II 216. 组合总和 III |
||
排列问题 | 46. 全排列 47. 全排列 II |
||
子集问题 | 78. 子集 90. 子集 II 491. 递增子序列 |
||
切割问题 | 131. 分割回文串 93. 复原 IP 地址 |
||
搜索问题 | 51. N 皇后 37. 解数独 |
LC链接:77.组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4],]
start
(indexStart
) 来逐层缩小搜索范围。比如这层选择了 i
,那么递归到下层时,应该从 i + 1
开始进行迭代。path
中的元素数量等于 k
时,即可结束。import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList();
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int start) {
// 当 path 中的元素数量等于k时,返回
if (path.size() == k) {
res.add(new ArrayList(path));
return;
}
// 下层遍历从 i+1 开始
for (int number = start; number <= n; number++) {
path.addLast(number);
backtracking(n, k, number + 1);
path.removeLast();
}
}
}
path
中,也不足以使得 path.size() == k
时,即可结束搜索(当前层及后续层级)。比如n = 5, k = 4,表示需要从 [1, 2, 3, 4, 5] 中选取4个元素。当程序运行到某个时刻,假设此时path.size()
为1,那么还需要3个元素添加到 path
中才能满足 k = 4 这个条件,因此就不能从 4 及 4 以后开始进行遍历,因为即使把所有这些元素加入到path中,也无法满足 k = 4 这个条件。path.size()
表示当前路径元素的个数k - path.size()
表示还需要的元素个数n - (k - path.size()) + 1
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> combine(int n, int k) {
res = new ArrayList();
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtracking(n, k, 1);
return res;
}
private void backtracking(int n, int k, int start) {
// 当 path 中的元素数量等于k时,返回
if (path.size() == k) {
res.add(new ArrayList(path));
return;
}
// 下层遍历从 i+1 开始
// 剪枝优化
for (int number = start; number <= n - (k - path.size()) + 1; number++) {
path.addLast(number);
backtracking(n, k, number + 1);
path.removeLast();
}
}
}
LC链接:39.组合总和
给定一个无重复元素的数组 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] ]
indexStart
缩小搜索范围,但是本题中每个元素可以使用次数的不限,所以下一次遍历的indexStart
等于本次遍历的indexStart
,而不需要加1target
是由原 target
一路减去 path
中的数字而传递下来的,所以当 target
小于等于 0 的时候,退出。特别地,当 target
等于0 的时候说明找到了和为原 target
的路径,需要将路径添加到最终的 res
中。import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int remain, int indexStart) {
// target小于0 退出
if (remain < 0) return;
// target等于于0,添加路径、退出
if (remain == 0) {
res.add(new ArrayList(path));
return;
}
for (int i = indexStart; i < candidates.length; i++) {
path.addLast(candidates[i]);
// 每个数字可以使用无限次,所以下次还从i开始遍历即可
backtracking(candidates, remain - candidates[i], i);
path.removeLast();
}
}
}
remain < candidates[i]
,则当candidates[i]或者 candidates[i]
之后的数字再加入 path
中,会导致 path
中的数字和超过target
,所以一旦出现 remain < candidates[i]
,即可终止。代码如下:import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates == null || candidates.length == 0 || target <= 0) return res;
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int remain, int indexStart) {
// target小于0 退出
if (remain < 0) return;
// target等于于0,添加路径、退出
if (remain == 0) {
res.add(new ArrayList(path));
return;
}
// 剪枝 remain >= candidates[i],如果 remain < candidates[i] 说明后续的数再加入path会导致path数字和超过原target
for (int i = indexStart; i < candidates.length && remain >= candidates[i]; i++) {
path.addLast(candidates[i]);
// 每个数字可以使用无限次,所以下次还从i开始遍历即可
backtracking(candidates, remain - candidates[i], i);
path.removeLast();
}
}
}
剪枝后,LC上运行时间从14ms降到3ms
LC链接:40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明: 所有数字(包括目标数)都是正整数。 解集不能包含重复的组合。
示例 1: 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
示例 2: 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]
indexStart
来逐层缩小搜索范围:该题是求组合,每个索引对应的元素只能使用一次,虽然数组中的元素可能重复(如例子 [10,1,2,7,6,1,5]
中,索引1和索引5对应的元素都是1),但是最终结果集中的每个组合都是对应索引只使用了一次,比如[1, 1, 6]
里的两个1分别对应[10,1,2,7,6,1,5]
数组索引1和索引5的位置。[2, 2, 1]
、[2, 1, 2]
的组合,但其实这两个算作一个组合。当然,也可以当把这两个组合都求出来后,在最后添加进结果集的时候再去重,但是这样做了很多无用功,势必会超时。而排序后,上述两个组合都变成 [1, 2, 2]
,这也能够在搜索过程中进行去重。树层去重1:使用used 数组:
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
backtracking(candidates, target, 0, used);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart, boolean[] used) {
if (target == 0) {
res.add(new ArrayList(path));
return;
}
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
used[i] = true;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1, used);
path.removeLast();
used[i] = false;
}
}
}
树层去重2: 三数之和的思想,其实这种思想有类似点 15. 三数之和 ,不管是求和的目的,还是去重的逻辑。
if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart) {
// target 每添加一个元素到 path,target 都会减去悉新加入元素的值,当 target 为0的时候,说明 path 里元素和等于 target
if (target == 0) {
res.add(new ArrayList(path));
return;
}
// 每次从 indexStart 开始遍历
// 且由于数组排序过,当剩余的 target < candidates[i]时,说明再将 candidates[i] 加入path,path里的元素和将会大于 target,
// 所以对于candidates[i] candidates[i+1] ... 不用再遍历
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
// 树层去重,注意 i > indexStart
if (i > indexStart && candidates[i] == candidates[i - 1]) continue;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1);
path.removeLast();
}
}
}
树层去重3:桶的思想,由于 1 <= candidates[i] <= 50
,我们为每一层创建一个桶(数组),如果每一层上某个数字已经使用过,就跳过
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
backtracking(candidates, target, 0);
return res;
}
private void backtracking(int[] candidates, int target, int indexStart) {
if (target == 0) {
res.add(new ArrayList(path));
return;
}
// 1 <= candidates[i] <= 50 记录每层数字是否使用过
boolean[] used = new boolean[51];
for (int i = indexStart; i < candidates.length && target >= candidates[i]; i++) {
if (used[candidates[i]] == true) continue;
used[candidates[i]] = true;
path.addLast(candidates[i]);
backtracking(candidates, target - candidates[i], i + 1);
path.removeLast();
}
}
}
indexStart
缩小搜索范围,也是为了避免结果集中每个组合中的元素重复;LC链接:216. 组合总和 III
找出所有相加之和为 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]]
start
(indexStart
)。由于题目中提到 “每种组合中不存在重复的数字”,所以需要利用 indexStart
来缩小搜索范围确保元素的之前使用过的元素不会再选取到。至于是使用 start
还是 indexStart
取决于我们在 for
循环中是使用索引进行遍历还是使用元素进行遍历。显然,本题直接使用元素进行遍历。path
中的元素个数等于k时,需要退出。并且当 remain
刚好为0时,说明 path
中所有数相加等于 n
,所以此时需要将 path
添加到 res
中。import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.ArrayDeque;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path;
public List<List<Integer>> combinationSum3(int k, int n) {
// 固定path的大小,防止运行中反复扩容带来的性能损失
path = new ArrayDeque(k);
backtrackin(k, n, 1);
return res;
}
private void backtrackin(int k, int remain, int start) {
// 元素达到 k 个,需要返回。特别地,如果此时 remain 刚好为0,需要将路径 path 添加到 res 中
if (path.size() == k) {
if (remain == 0) {
res.add(new ArrayList(path));
}
return;
}
// remain >= num 进行剪枝
for (int num = start; num <= 9 && remain >= num; num++) {
path.addLast(num);
backtrackin(k, remain - num, num + 1);
path.removeLast();
}
}
}
indexStart
(start
) 来在递归中缩小搜索范围,避免组合中出现重复元素;LC链接:46. 全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例: 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
indexStart
:排列由于需要把数组中的数字(1、2、3)全部用到,所以不能像组合、子集问题一样利用 indexStart
来逐步缩小问题范围,而是需要在每次递归时做完整的遍历(即从索引0开始遍历),把没有用到的元素添加到 path
中。path
中元素数量等于原数组中元素数量时,说明原数组元素远不用完,返回。usedPath
数组来记录一条路径(树枝)上对应索引元素的使用情况。import java.util.List;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> permute(int[] nums) {
boolean[] usedPath = new boolean[nums.length];
backtracking(nums, usedPath);
return res;
}
// 排列,数组里的元素都要用到且只能用一次,不用像组合一样使用indexStart缩小取值范围
// 而是使用 usedPath 数组来标记每一条路径(树枝)上元素的使用情况,如果已经在该路径上使用过就跳过
private void backtracking(int[] nums, boolean[] usedPath) {
// 退出条件:当路径元素跟数组元素相等时
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
// 排列每次都从0开始遍历,利用usedPath来确保每一条路径上元素的唯一性
for (int i = 0; i < nums.length; i++) {
if (usedPath[i] == true) continue;
usedPath[i] = true; // 记录该下标的元素在当前路径上已经使用过
path.addLast(nums[i]); // 将元素添加到当前路径上
backtracking(nums, usedPath); // 带着当前路径元素的使用记录,继续去(从下标0开始)遍历
path.removeLast(); // 回溯路径
usedPath[i] = false; // 回溯路径元素使用记录
}
}
}
indexStart
,而是使用 usedPath
完成路径上的去重, usedPath
记录的是数组元素在每条路径上的使用情况——元素下标LC链接:47. 全排列 II
给定一个可包含重复数字的序列 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
used
数组完成数层和树枝去重class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
boolean[] used = new boolean[nums.length];
Arrays.sort(nums);
backtracking(nums, used);
return res;
}
// 思路:在全排列的基础上增加树层去重
private void backtracking(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 树层去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
// 树枝去重
if (used[i] == true) continue;
used[i] = true;
path.addLast(nums[i]);
backtracking(nums, used);
path.removeLast();
used[i] = false;
}
}
}
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> permuteUnique(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
boolean[] usedPath = new boolean[nums.length];
// Arrays.sort(nums);
backtracking(nums, usedPath);
return res;
}
// 思路:在全排列的基础上增加树层去重
private void backtracking(int[] nums, boolean[] usedPath) {
if (path.size() == nums.length) {
res.add(new ArrayList(path));
return;
}
boolean[] usedLayer = new boolean[21];
for (int i = 0; i < nums.length; i++) {
// 树层去重
if (usedLayer[nums[i] + 10] == true) continue;
// 树枝去重
if (usedPath[i] == true) continue;
usedLayer[nums[i] + 10] = true;
usedPath[i] = true;
path.addLast(nums[i]);
backtracking(nums, usedPath);
path.removeLast();
usedPath[i] = false;
}
}
}
LC链接:78. 子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
indexStart
来确保取过的元素不会重复添加。但是组合问题是到叶子结点才进行收集,而子集问题是收集所有节点。class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> subsets(int[] nums) {
res = new ArrayList();
path = new ArrayDeque(nums.length);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStart) {
// 子集问题不需要主动退出,当遍历完数组自然结束即可
res.add(new ArrayList(path));
for (int i = indexStart; i < nums.length; i++) {
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
90. 子集 II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: [1,2,2] 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
used
数组去重。原数组有重复,而要求的集合中不允许有重复,画树形图分析后可知需要树层去重。使用 used
数组结合 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
条件完成树层去重class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
backtracking(nums, 0, used);
return res;
}
private void backtracking(int[] nums, int indexStart, boolean[] used) {
res.add(new ArrayList(path));
for (int i = indexStart; i < nums.length; i++) {
// 树层去重
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
used[i] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1, used);
path.removeLast();
used[i] = false;
}
}
}
usedLayer
数组用来在遍历某一层(for 循环)时,记录当前层的数字使用情况,由于题目限定 -10 <= nums[i] <= 10,所以usedLayer
大小为 21,相当于把 [-10,10] 平移到 [0, 20],然后以下标来进行统计,下标 0 对应 nums 中的-10,下标 1 对应 nums 中的-9 … 下标 20 对应 nums 中的10。class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStart) {
res.add(new ArrayList(path));
boolean[] usedLayer = new boolean[21];
for (int i = indexStart; i < nums.length; i++) {
// 树层去重,桶的思想
if (usedLayer[nums[i] + 10] == true) continue;
usedLayer[nums[i] + 10] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
LC链接:491. 递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是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、大于才能添加到path中
class Solution {
List<List<Integer>> res = new ArrayList();
Deque<Integer> path = new LinkedList();
public List<List<Integer>> findSubsequences(int[] nums) {
// 不能排序,求递增子序列,如果排序就改变了数组原有的顺序
backtracking(nums, 0);
return res;
}
private void backtracking(int[] nums, int indexStrat) {
if (path.size() >= 2) {
res.add(new ArrayList(path));
}
boolean[] usedLayer = new boolean[201];
for (int i = indexStrat; i < nums.length; i++) {
// 树层去重
if (usedLayer[nums[i] + 100] == true) continue;
// 确保递增
if (path.isEmpty() || nums[i] >= path.getLast()) {
usedLayer[nums[i] + 100] = true;
path.addLast(nums[i]);
backtracking(nums, i + 1);
path.removeLast();
}
}
}
}
LC链接:698. 划分为k个相等的子集
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
used 数组既可以用来树层去重,也可以用来树枝去重,为什么 46. 全排列 树枝去重时使用 if (i > 0 && nums[i] == nums[i - 1] && usedPath[i - 1] == true) continue;
不对? 因为46题中的元素没有重复的?
说好的树层去重需要排序,但是为什么 47. 全排列 II 利用桶的思想去重时就不用排序?因为[2, 1, 2] 和 [2, 2, 1] 是不同的排列,但是属于同一个组合(子集)。
2.3、3.2、4.2三个题结合看,总结提炼树层去重的共性!
树层去重里的层指的是具有直接共同父节点的层,而不是层次遍历中指的一整层!