🌈🌈😄😄
欢迎来到茶色岛独家岛屿,本期将为大家揭晓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
https://blog.csdn.net/weixin_62275996/article/details/128797170?spm=1001.2014.3001.5501
以上都是关于回溯问题的一些思路
78. 子集
一、力扣示例
78. 子集 - 力扣(LeetCode) https://leetcode.cn/problems/subsets/
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/
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/
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/
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/
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/
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;
    }
}
}





















