【算法】回溯法解决排列、组合、子集问题套路总结

回溯算法详解:排列组合子集问题
本文详细介绍了如何使用回溯算法解决排列、组合、子集问题,包括元素无重不可复选、可重不可复选及无重可复选三种情况,并通过LeetCode题目实例解析解题模板及其变种。同时,文章提供了具体的Java代码实现,帮助读者理解并掌握回溯算法在不同场景下的应用。

本文是在刷题的过程中,对此类型的算法题进行的一个浅显的总结。故而,本文不在对概念进行解释,主要集中在解题的方法和思路上。

总的来说,回溯算法解题的过程,就是在决策树上遍历的过程。

问题类型

对于排列、组合、子集问题,我们可以根据题目的限制条件“元素是否重复”、“是否允许重复选择”,大致的将其分为三类:

①元素无重不可复选,即集合中的元素都是唯一的,每个元素最多只能被使用一次;

②元素可重不可复选,即集合中的元素可以存在重复,每个元素最多只能被使用一次;

③元素无重可复选,即集合中的元素都是唯一的,每个元素可以被使用若干次;

对于元素可以重复、可复选的情况,由于可复选,我们可以对其去重之后计算,此时等同于情况三

以上分类参考:【一文秒杀所有排列组合子集问题

下图就是对于所有情况的搜索树:

回溯法搜索树

从根节点到叶子节点的每一条路径就是对应的一个解(严格来说,应该是从根节点的儿子节点到叶子节点的路径,因为根节点代表原始的集合)

解题模板

回溯法的基本模板总结如下:

private void backtrack(选择列表){
    // 1、判断是否满足结束条件,若满足,则添加进入结果集
    // 2、遍历剩余可选择的列表
    for (选择 in 选择列表) {
        // 3、选择条件(视题意而定),是否选择当前的“选择”节点
        // 4、递归调用
        backtrack(选择列表);
        // 5、撤销上一步的选择,回溯到上一步
    }
}

问题分析

总的来看,对于不同的情况只需要更改回溯算法模板的步骤1、步骤2(for循环中的条件)以及步骤3 即可。现在,我们具体分析上文所说的三种情况,以及在三种不同的情况中如何去使用解题模板。

①元素无重不可复选

Leetcode 78题 为例:

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能包含重复的子集。你可以按 任意顺序 返回解集。

这种情况就属于最基本的情况,我们可以传递一个参数:当前元素的索引,令下一次搜索的索引为当前的索引+1,使得每次只遍历集合中当前索引之后的值从而避免重复选择(产生重复子集)。其余使用模板代入即可。

class Solution {
    private LinkedList<Integer> list = new LinkedList<>();
    private List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }
    // 回溯
    private void backtrack(int[] nums, int index) {
        // 满足条件,添加进入结果集
        res.add(new ArrayList<>(list));
        for (int i = index; i < nums.length; i++) {
            // 选择
            list.add(nums[i]);
            // 递归调用
            backtrack(nums, i + 1);
            // 撤销选择
            list.removeLast();
        }
    }
}

此问题的其他变种情况:如获取集合中,所有长度为 n 的子集或和等于 target 的子集。

以长度为 n 为例,长度为 n,即第n次选择的结果集。此时,只需要更改步骤一的结束条件即可,当满足 list.size() == n 时,添加进入结果集。

②元素可重不可复选

Leetcode 90题 为例:

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

此时,我们只需要对相同值的树枝剪枝即可。所以,我们可以先对集合中的元素进行排序,然后只需要判断下一次选择时,相邻的元素是否相等,若相等则跳过,进入下一次循环。

class Solution {
    private LinkedList<Integer> list = new LinkedList<>();
    private List<List<Integer>> res = new LinkedList<>();
    
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 先排序
        Arrays.sort(nums);
        backtrack(nums, 0);
        return res;
    }
    // 回溯法
    private void backtrack(int[] nums, int start){
        res.add(new ArrayList<>(list));
        for (int i = start; i < nums.length; i++) {
            // 跳过重复的元素(并且确保边界条件index=0时,不溢出)
            if (i> start && nums[i] == nums[i-1]) {
                continue;
            }
            list.add(nums[i]);
            backtrack(nums, i+1);
            list.removeLast();
        }
    }
}

对于部分特殊情况,如不允许集合重复,可能还需要额外的剪枝
Leetcode 47题 为例:
此题需要通过保证集合中的数据相对固定,来进行去重。比如:集合{1,2,2,3}中,我们可以规定 2 和 2’ 在解集中,只能令 2 在前,2’ 在后。如果,2’ 先出现,则不加入解集中。
所以,我们通过使用额外的空间记录元素的访问标记,通过比较标记从而实现对位置的判定。

class Solution {
    private LinkedList<Integer> list = new LinkedList<>();
    private List<List<Integer>> res = new LinkedList<>();
    private boolean[] flag;
    
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        // 用标记数组标记元素是否使用过
        flag = new boolean[nums.length];
        backtrack(nums);
        return res;
    }
    private void backtrack(int[] nums){
        if (list.size() == nums.length) {
            res.add(new ArrayList<>(list));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            // 确保元素不会重复使用
            if (flag[i]) {
                continue;
            }
            // 固定相同的元素在排列中的相对位置
            // 相同值的两个元素 a 和 a', 只需要保持结果集中的a 排列一定在 a' 以前, 就可以保证集合无重复
            if (i > 0 && nums[i] == nums[i-1] && !flag[i-1]) {
                continue;
            }
            list.add(nums[i]);
            flag[i] = true;
            backtrack(nums);
            list.removeLast();
            flag[i] = false;
        }
    }
}

③元素无重可复选

Leetcode 39题 为例:

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。 candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对照上文第一种情况,我们是通过控制 index+1,令得下一次搜索只遍历集合中当前索引之后的值从而避免重复选择,所以,反过来,如果我们想让当前值被重复使用,只需要把传递的参数 index +1 -> index即可重复使用当前值。

class Solution {
    private LinkedList<Integer> list = new LinkedList<>();
    private List<List<Integer>> res = new LinkedList<>();
    
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtrack(candidates, 0, target);
        return res;
    }

    // 回溯法
    private void backtrack(int[] nums, int start, int target){
        // 结束条件
        if (target == 0) {
            res.add(new LinkedList<>(list));
            return;
        }
        if(target < 0){
            return;
        }
        for (int i = start; i < nums.length; i++) {
            // 选择
            list.add(nums[i]);
            backtrack(nums, i, target-nums[i]);
            // 撤销选择
            list.removeLast();
        }
    }
}

总的来说,不论那种情况,基本上只需要在模板的基础上更改步骤1:结束条件和步骤3:选择当前节点即可。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值