给定一个候选人编号的集合 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)
回溯算法 + 剪枝(Java、Python) - 组合总和 II - 力扣(LeetCode)
组合总和 II - 组合总和 II - 力扣(LeetCode)