回溯算法秒杀所有排列-组合-子集问题

🌈🌈😄😄

欢迎来到茶色岛独家岛屿,本期将为大家揭晓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博客icon-default.png?t=MBR7https://blog.csdn.net/weixin_62275996/article/details/128797170?spm=1001.2014.3001.5501

 以上都是关于回溯问题的一些思路

78. 子集

一、力扣示例

78. 子集 - 力扣(LeetCode)icon-default.png?t=MBR7https://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)icon-default.png?t=MBR7https://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)icon-default.png?t=MBR7https://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)icon-default.png?t=MBR7https://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)icon-default.png?t=MBR7https://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)icon-default.png?t=MBR7https://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;
    }
}
}

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

茶色岛^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值