回溯算法总结---子集组合类问题

本文深入探讨了深度优先搜索(DFS)与回溯算法的区别,指出回溯算法在DFS基础上增加了状态重置。文章列举了何时使用回溯算法,并详细阐述了如何构建回溯算法的步骤。通过分析LeetCode的78.子集、90.子集II和39.组合总和三道题目,展示了回溯算法在解决集合子集、去重子集和组合总和问题的应用,强调了在处理重复元素时的去重策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.DFS 和回溯算法区别

DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置

2.何时使用回溯算法

当问题需要 "回头",以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止

3.怎么样写回溯算法(从上而下,※代表难点,根据题目而变化)
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择

4.回溯问题的类型

5.例题及个人理解

78. 子集 - 力扣(LeetCode)

子集类题目,答案从空集开始,集合中包含一个元素的,集合中包含俩个元素的.......集合中包含给定数组个数个元素的,所以用了一个for循环,设置一个 level 变量,值为从0开始一直到给定数组个数作为限制该子集中只能包含 level 个元素

子集问题的选择列表,是上一条选择路径之后的数,所以设置变量 start 表示每次循环递归的起始位置,每次循环都让起始位置 start + 1 以达到从上一条选择路径之后的数。

递归结束条件:新子集数组的个数满足当前循环限制的该子集中只能包含 level 个元素 即 新子集数组个数 等于 该层循环 level

var subsets = function (nums) {
    const res = [];
    // path:每一个子集数组,level:该层子集集合应该有的个数 start:从原集合中第几个位置开始 
    const backtrack = (path, level, start) => {
        if (path.length === level) {
            res.push(path);
            return;
        }
        // 每次循环递归都让起始位置+1  即 i+1
        for (let i = start; i < nums.length; i++) {
            backtrack(path.concat(nums[i]), level, i+1);
        }
    }
    // i 从 0 开始 i等于0的时候就是空集
    for (let i = 0; i <= nums.length; i++) {
        // 子集数组,该层数组应有几个元素 从原数组第几个位置开始
        backtrack([], i, 0);
    }
    return res;
};

 90. 子集 II - 力扣(LeetCode)

因为给定集合中有重复的元素,所以核心是去重,所谓去重,其实就是使用过的元素不能重复选取,“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过

为了实现去重,就要先保证给定的集合数组是有序的,所以要先排序

裁剪:由递归树可以看出,求有重复元素集合的子集,就是在上一题基础上额外删去同一树层上的重复集合,即在遍历当前选择列表当中,如果后一个列表元素与前一个列表元素相同,则不做新的添加。

而循环递归时指定从原集合第几个元素开始(start参数)该次递归的选择列表就是原集合从 start开始到集合最后一个元素。

想有前后选择列表元素的比较,那么最少也是循环到了选择列表的第二个元素了。即 i > start 

递归结束条件:新子集数组的个数满足当前循环限制的该子集中只能包含 level 个元素 即 新子集数组个数 等于 该层循环 level

var subsetsWithDup = function (nums2) {
    // 保证给定集合数组有序
    const nums = nums2.sort((a, b) => {
        return a - b;
    })
    const res = [];
    const backtrack = (path, level, start) => {
        if (path.length === level) {
            res.push(path);
            return;
        }
        for (let i = start; i < nums.length; i++) {
            // 满足后一个选择列表元素等于前一个选择列表元素,则不做添加操作,直接跳过
            if (i > start && nums[i] === nums[i - 1]) {
                continue;
            }
            backtrack(path.concat(nums[i]), level, i + 1);
        }
    }
    for (let i = 0; i <= nums.length; i++) {
        // 数组,限制该子集中有几个元素,循环开始下标
        backtrack([], i, 0);
    }
    return res;
};

39. 组合总和 - 力扣(LeetCode)

组合的题目是允许重复使用给定集合中的元素的,只要最终结果能够满足给定的 target 值,不限制用了多少个子元素和重复使用了几次子元素。

因为要计算不断添加元素的总和是否已经满足 target 所以每次循环递归还要传递一个 sum 参数

递归函数代码的整体模板与上俩题类似,区别在于本题允许重复使用元素且无需限制子集的元素个数,所以递归循环起始元素不需要每次 + 1,就从上一个元素开始继续递归循环

裁剪:sum 的值大于 target 时,不进行任何操作

递归结束条件:sum 的值等于 target 时 推入 res 数组

var combinationSum = function (candidates, target) {
    const res = [];
    let sum = 0;
    // 路径数组,当前数组总和,起始位置,目标值
    const backtrack = (path, sum, start, target) => {
        if (sum >= target) {
            if (sum === target) {
                res.push(path);
            }
            return;
        }
        for (let i = start; i < candidates.length; i++) {
            // 因为允许重复 所以start参数的值不需要额外 +1 直接从当前元素开始继续循环
            backtrack(path.concat(candidates[i]), sum + candidates[i], i, target)
        }
    }
    backtrack([], sum, 0, target);
    return res;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值