数据结构进阶篇,数组组合专题

39. 组合总和

  「题目:」

  给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

  candidates 中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。

  「示例:」

  输入:candidates = [2,3,6,7], target = 7,输出:[[2,2,3],[7]]。

  「解题思路:」

  寻找可行解的题型,可以使用搜索回溯法,其本质上就是暴力枚举,时间复杂度会非常高,通常需要针对其回溯的条件进行剪枝优化。

  • 枚举的过程中需要记录当前剩余的 target 值,当前选择的元素以及当前数组的下标。

  • 当超出数组下标或者剩余 target 值为零时,即为递归终止条件。

  • 枚举过程中,可以直接跳过该元素,但是如果当前元素与 target 的差值大于等于零,可以选择该元素。

  时间复杂度:O(2^n),空间复杂度:O(n)。

const combinationSum = (candidates, target) => {
    const ans = [];

    const dfs = (target, combine, index) => {
        if (target === 0) {
            ans.push(combine);
            return;
        }

        if (index === candidates.length) {
            return;
        }

        // 不选择当前数字
        dfs(target, combine, index + 1);

        // 选择该数字
        if (target - candidates[index] >= 0) {
            dfs(target - candidates[index], [...combine, candidates[index]], index);
        }
    }

    dfs(target, [], 0);

    return ans;
}

  相比较上述回溯的实现方式,下面这种方式更加优雅:

const combinationSum = (candidates, target) => {
    const ans = [];
    const path = [];

    const backtrack = (start, sum) => {
        if (sum === target) {
            ans.push([...path]);
            return;
        }

        if (sum > target) {
            return;
        }

        for (let i = start; i < candidates.length; i++) {
            // 选择
            path.push(candidates[i]);
            sum += candidates[i];
            backtrack(i, sum);
            // 不选择。状态回滚
            path.pop();
            sum -= candidates[i];
        }
    }

    backtrack(0, 0);

    return ans;
}

40. 组合总和 II

  「题目:」

  给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

  candidates 中的每个数字在每个组合中只能使用 一次 。

  注意:解集不能包含重复的组合。

  「示例:」

  输入: candidates = [10,1,2,7,6,1,5], target = 8,输出:[[1,1,6],[1,2,5],[1,7],[2,6]]。

  「解题思路:」

  这道题相比较上一题加了一些限制条件:

  • 每个数组只使用一次。

  • 最终能到的组合不重复。

  只要在枚举的过程中都移动 index 下标即可保证每个数字只使用一次。

  针对不重复组合,第一时间会想到通过哈希表来对结果去重,但是这种方式并不能降低算法本身的时间复杂度,所以需要分析回溯的条件,减少重复回溯的消耗,从而达到剪枝优化的目的。

  出现重复组合的根本原因在于:每层枚举的过程中,重复处理了已经处理过的数字。

  为了能够方便过滤掉本层重复处理的数字,需要对数组进行排序预处理。

  时间复杂度:O(2^n),空间复杂度:O(n)。

const combinationSum2 = function(candidates, target) {
    const ans = [];
    const path = [];

    candidates.sort((a, b) => a - b);

    const backtrack = (start, sum) => {
        if (sum === target) {
            ans.push([...path]);
            return;
        }

        if (sum > target) {
            return;
        }

        for (let i = start; i < candidates.length; i++) {
            // 剪枝:过滤掉同层相同的元素
            if (i - 1 >= start && candidates[i - 1] === candidates[i]) {
                continue;
            }
            // 选择
            path.push(candidates[i]);
            sum += candidates[i];
            backtrack(i + 1, sum);
            // 不选择。状态回滚
            path.pop();
            sum -= candidates[i];
        }
    }

    backtrack(0, 0);

    return ans;
};

216. 组合总和 III

  「题目:」

  找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

  说明:

  • 所有数字都是正整数。

  • 解集不能包含重复的组合。

  「示例:」

  输入: k = 3, n = 7,输出: [[1,2,4]]。

  「解题思路:」

  相比较上一题,本道题虽然抽象了目标值和组合元素个数的限制,但是并没有改变此类问题的通用解法,只不过是变更了递归终止的条件。

  时间复杂度:O(2^9),空间复杂度:O(n)。

const combinationSum3 = function(k, n) {
    const candidates = [1, 2, 3, 4, 5, 6, 7, 8, 9];

    const ans = [];

    const path = [];

    const backtrack = (start, sum) => {
        if (path.length === k && sum === n) {
            ans.push([...path]);
            return;
        }

        if (path.length > k || sum > n) {
            return;
        }

        for (let i = start; i < candidates.length; i++) {
            path.push(candidates[i]);
            sum += candidates[i];

            backtrack(i + 1, sum);

            path.pop();
            sum -= candidates[i];
        }
    }

    backtrack(0, 0);
    
    return ans;
};

377. 组合总和 Ⅳ

  「题目:」

  给你一个由不同整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

  题目数据保证答案符合 32 位整数范围。

  「示例:」

  输入:nums = [1,2,3], target = 4,输出:7。

  「解题思路:」

  本道题求解最终组合的排列个数,如果还是采用回溯枚举的方式,时间复杂度会非常的高,所以本题只要求求解可行解的个数,而非具体的排列。

  求可行解个数的题型,一般采用动态规划来解决,它相比较回溯,主要快在能够消除重叠子问题。

  设 dp[target] 表示形成 target 目标值的排列个数:

  • 边界情况:当 target = 0 时,只有不选择任何元素,才能使得元素之和等于 0,所以 dp[0] = 1。

  • 状态转移方程:遍历 sum 从 1 到 target,再嵌套遍历 nums,如果 num <= sum,那么当前 sum 的排列数就包含 sum - num 的排列数,即 dp[sum] += dp[sum - num]。

  在动态规划的过程中,通过 dp[sum] 临时状态的记忆来消除了回溯中 backtrack(sum) 这样的重叠子问题的消耗,这是典型的空间换时间的优化手段。

  时间复杂度:O(target*n),空间复杂度:O(target)。

const combinationSum4 = function(nums, target) {
    const dp = Array(target + 1).fill(0);

    dp[0] = 1;

    for (let sum = 1; sum <= target; sum++) {
        for (const num of nums) {
            if (num <= sum) {
                dp[sum] += dp[sum - num];
            }
        }
    }

    return dp[target];
}

写在最后

  「感谢您能耐心地读到这里,如果本文对您有帮助,欢迎点赞、分享、或者关注下方的公众号哟。」

  相关链接:

  • 《39. 组合总和》 https://leetcode-cn.com/problems/combination-sum/

  • 《40. 组合总和 II》 https://leetcode-cn.com/problems/combination-sum-ii/

  • 《216. 组合总和 III》 https://leetcode-cn.com/problems/combination-sum-iii/

  • 《377. 组合总和 Ⅳ》 https://leetcode-cn.com/problems/combination-sum-iv/

fa03dc337c1b44eb4b3cd94b4e11cac6.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值