🌈🌈😄😄
欢迎来到茶色岛独家岛屿,本期将为大家揭晓LeetCode 78. 子集 90. 子集 II 77. 组合 39. 组合总和 40. 组合总和 II 47. 全排列 II,做好准备了么,那么开始吧。
🌲🌲🐴🐴
重点:
解决一个回溯问题,实际上就是一个决策树的遍历过程,站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
[2]
就是「路径」,记录你已经做过的选择;[1,3]
就是「选择列表」,表示你当前可以做出的选择;「结束条件」就是遍历到树的底层叶子节点,这里也就是选择列表为空的时候。
思路与解法
- 求子集与组合等问题时,函数用start
- 求排列时用used[i]排除不合法选择
- for循环也就是在循环选择列表,从左到右排放
- 递归是从上到下搜寻
- 回溯是从下到上回归
类似解法可看(7条消息) 回溯详解 LeetCode 46. 全排列 51. N 皇后 52. N皇后 II_茶色岛^的博客-CSDN博客https://blog.csdn.net/weixin_62275996/article/details/128797170?spm=1001.2014.3001.5501
以上都是关于回溯问题的一些思路
78. 子集
一、力扣示例
78. 子集 - 力扣(LeetCode)https://leetcode.cn/problems/subsets/
二、解决办法
回溯
我们使用 start
参数控制树枝的生长避免产生重复的子集,用 track
记录根节点到每个节点的路径的值,同时在前序位置把每个节点的路径值收集起来,完成回溯树的遍历就收集了所有子集。
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
// 主函数
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return res;
}
// 回溯算法核心函数,遍历子集问题的回溯树
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 做选择
track.addLast(nums[i]);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(nums, i + 1);
// 撤销选择
track.removeLast();
}
}
}
90. 子集 II
一、力扣示例
90. 子集 II - 力扣(LeetCode)https://leetcode.cn/problems/subsets-ii/
二、解决办法
我们需要进行剪枝,如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历,体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1]
,则跳过。
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
backtrack(nums, 0);
return res;
}
void backtrack(int[] nums, int start) {
// 前序位置,每个节点的值都是一个子集
res.add(new LinkedList<>(track));
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,值相同的相邻树枝,只遍历第一条
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
track.addLast(nums[i]);
backtrack(nums, i + 1);
track.removeLast();
}
}
}
77. 组合
一、力扣示例
77. 组合 - 力扣(LeetCode)https://leetcode.cn/problems/combinations/
类型:组合(元素无重不可复选)
二、解决办法
类似于给你输入一个数组 nums = [1,2..,n]
和一个正整数 k
,请你生成所有大小为 k
的子集。
上面求子集问题是让你求所有子集,就是把所有节点的值都收集起来;
现在你只需要把第 k 层(根节点视为第 0 层)的节点收集起来,就是大小为 k的所有组合,
反映到代码上,只需要稍改 base case,控制算法仅仅收集第 k
层节点的值即可。
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯算法的递归路径
LinkedList<Integer> track = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backtrack(1, n, k);
return res;
}
void backtrack(int start, int n, int k) {
// base case
if (k == track.size()) {
// 遍历到了第 k 层,收集当前节点的值
res.add(new LinkedList<>(track));
return;
}
// 回溯算法标准框架
for (int i = start; i <= n; i++) {
// 选择
track.add(i);
// 通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(i + 1, n, k);
// 撤销选择
track.removeLast();
}
}
}
39. 组合总和
一、力扣示例
39. 组合总和 - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum/
二、解决办法
- 这个
i
从start
开始,那么下一层回溯树就是从start + 1
开始,从而保证nums[start]
这个元素不会被重复使用 - 如果我想让每个元素被重复使用,我只要把
i + 1
改成i
即可 - 这相当于给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用
- 我们的递归函数需要设置合适的 base case 以结束算法,即路径和大于
target
时就不再遍历
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯的路径
LinkedList<Integer> track = new LinkedList<>();
// 记录 track 中的路径和
int trackSum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
if (candidates.length == 0) {
return res;
}
backtrack(candidates, 0, target);
return res;
}
// 回溯算法主函数
void backtrack(int[] nums, int start, int target) {
// base case,找到目标和,记录结果
if (trackSum == target) {
res.add(new LinkedList<>(track));
return;
}
// base case,超过目标和,停止向下遍历
if (trackSum > target) {
return;
}
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 选择 nums[i]
trackSum += nums[i];
track.add(nums[i]);
// 递归遍历下一层回溯树
// 同一元素可重复使用,注意参数
backtrack(nums, i, target);
// 撤销选择 nums[i]
trackSum -= nums[i];
track.removeLast();
}
}
}
40. 组合总和 II
一、力扣示例
40. 组合总和 II - 力扣(LeetCode)https://leetcode.cn/problems/combination-sum-ii/
二、解决办法
对比子集问题的解法,只要额外用一个 trackSum
变量记录回溯路径上的元素和,然后将 base case 改一改即可解决这道题。
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
// 记录回溯的路径
LinkedList<Integer> track = new LinkedList<>();
// 记录 track 中的元素之和
int trackSum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if (candidates.length == 0) {
return res;
}
// 先排序,让相同的元素靠在一起
Arrays.sort(candidates);
backtrack(candidates, 0, target);
return res;
}
// 回溯算法主函数
void backtrack(int[] nums, int start, int target) {
// base case,达到目标和,找到符合条件的组合
if (trackSum == target) {
res.add(new LinkedList<>(track));
return;
}
// base case,超过目标和,直接结束
if (trackSum > target) {
return;
}
// 回溯算法标准框架
for (int i = start; i < nums.length; i++) {
// 剪枝逻辑,值相同的树枝,只遍历第一条
if ( i > start && nums[i] == nums[i - 1]) {
continue;
}
// 做选择
track.add(nums[i]);
trackSum += nums[i];
// 递归遍历下一层回溯树
backtrack(nums, i + 1, target);
// 撤销选择
track.removeLast();
trackSum -= nums[i];
}
}
}
47. 全排列 II
一、力扣示例
47. 全排列 II - 力扣(LeetCode)https://leetcode.cn/problems/permutations-ii/
二、解决办法
对比一下之前的标准全排列解法代码,这段解法代码只有两处不同:
1、对 nums
进行了排序。
2、添加了一句额外的剪枝逻辑。
类比输入包含重复元素的子集/组合问题,你大概应该理解这么做是为了防止出现重复结果。
注意排列问题的剪枝逻辑,和子集/组合问题的剪枝逻辑略有不同:新增了 !used[i - 1]
的逻辑判断。
如果用绿色树枝代表 backtrack
函数遍历过的路径,红色树枝代表剪枝逻辑的触发,那么 !used[i - 1]
这种剪枝逻辑得到的回溯树长这样。
三、代码实现
class Solution {
List<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
// 先排序,让相同的元素靠在一起
Arrays.sort(nums);
used = new boolean[nums.length];
backtrack(nums);
return res;
}
void backtrack(int[] nums) {
//触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
//排除不合法的选择
if (used[i]) {
continue;
}
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1])
//当 前一个元素与当前元素相同且未被选择时,不选择当前元素
{
continue;
}
//选择
track.add(nums[i]);
used[i] = true;
//递归遍历下一层回溯树
backtrack(nums);
//撤销
track.removeLast();
used[i] = false;
}
}
}