LeetCode 40. 组合总和 II

40. 组合总和 II

题目描述

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

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

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

示例 1:

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

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

解题思路

思路一: 回溯

由39.组合总和这道题区别在于: 每个数字在每个组合中只能使用一次, 即这道题中我们需要去重。

由39.组合总和这道题我们知道,数组 candidates 有序,也是 深度优先遍历 过程中实现「剪枝」的前提。
将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组 升序 排序,重复的元素一定不是排好序以后相同的连续数组区域的第 1 个元素。也就是说,剪枝发生在:同一层数值相同的结点第 2、3 … 个结点,因为数值相同的第 1 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 1 个结点更多,并且是第 1 个结点的子集。

因此去重代码如下:

if (i > startIndex && candidates[i] == candidates[i - 1]) {
    continue;
}

以上代码也是与39题剪枝优化版本代码中唯一区别。

完整实现代码如下:

/**
 * @param {number[]} candidates
 * @param {number} target
 * @return {number[][]}
 */
var combinationSum2 = function(candidates, target) {
    let res = [];
    // 排序是剪枝的前提
    candidates.sort((a, b) => a - b);
    backtrace(candidates,target,res,[] ,0)
    return res;
};

function backtrace(candidates,target,res,ans,startIndex){
    // 如果此时目标元素经过几次深度递归,出现负值,
    // 就说明,数组中不存在能相加等于目标数组的元素集合
    if(target < 0){
        return;
    }
    if(target === 0) {
        res.push([...ans]);
        return;
    }
    // 遍历元素,这里的i 必须要跟递归层数保持一致,要不要剪枝时,会照成重复元素
    for(let i = startIndex;i < candidates.length;i++){ 
        // 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
        if (target - candidates[i] < 0) {
            break;
        }

        // 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
        // 原因在有序的情况下,后面出现的元素都会相同,造成结果集也会相同
        if (i > startIndex && candidates[i] == candidates[i - 1]) {
            continue;
        } 

        // //将路径上的元素加入结果集合中
        ans.push(candidates[i])
        // 递归
        // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
        backtrace(candidates,target - candidates[i], res, ans, i + 1);
        // 回溯
        // 将元素进行删除,也叫剪枝,
        // 这里必须从队列的尾部开始删除,这样才能达到从底层逐层删除
        ans.pop()
    }
}
  • 时间复杂度: O ( 2 n × n ) O(2^n \times n) O(2n×n),其中 n 是数组 candidates 的长度。包括三部分:

    • 数组排序的时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)
    • 在最坏的情况下,数组中的每个数都不相同,在递归时,每个位置可以选或不选,如果数组中所有数的和不超过 target,那么 2 n 2^n 2n种组合都会被枚举到;
    • 在 target 小于数组中所有数的和时,但我们知道每得到一个满足要求的组合,需要 O(n) 的时间将其放入答案中,
      因此总时间复杂度 = O ( n log ⁡ n ) + O ( 2 n × n ) O(n \log n) + O(2^n \times n) O(nlogn)+O(2n×n) = O ( 2 n × n ) O(2^n \times n) O(2n×n)
  • 空间复杂度:O(n)

参考资料

回溯算法 + 剪枝(Java、Python) - 组合总和 II - 力扣(LeetCode)

组合总和 II - 组合总和 II - 力扣(LeetCode)

你可能感兴趣的:(LeetCode,1024程序员节,leetcode,回溯)