持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情
前言
今天的题目为两个中等题,一个是之前做过的就不在细说,另一个就是前一道题的变形,可以拿来和前一道题做一个对比。
每日一题
今天的题目是 40. 组合总和 II 以及 39.组合总和,难度都为中等,其中第二题前几天刚做过,今天就只是将两题的思路做一个对比,所以第二题的题目就不放了。
-
给定一个候选人编号的集合 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] ]
提示:
-
1 <= candidates.length <= 100
-
1 <= candidates[i] <= 50
-
1 <= target <= 30
题解
往期回溯练习题
组合回溯1 leetcode刷题记录-77. 组合(回溯基础)
组合回溯2 leetcode刷题记录-216. 组合总和 III(回溯剪枝)
组合回溯3 leetcode刷题记录-17. 电话号码的字母组合(回溯组合问题)
组合1重新分析
对于组合1来说,因为元素是可以重复使用的,所以在每一次递归的过程当中,可选的数组应该是不变的,这个递归分支是否结束应该变成用当前的总和是否大于等于target来判断,并且向上图中,5这个分支是取不到2的,因为这是组合,组合不考虑排序,所以5去取2和2来取5是完全相同的,也就是重复了,就可以不用进行,这样后面才不用进行去重。
然后剩下的就是解题了,按照以前的回溯三部曲来进行解题。
- 思考清楚递归函数需要的参数 2. 结束条件 3. 单层搜索逻辑
function combinationSum(candidates: number[], target: number): number[][] {
let res = []
let sum = 0
let path = []
let n = candidates.length
const backcarking = (index) => {
if(sum==target) {
res.push([...path])
return
}
for(let i = index; i<n;i++) {
sum+=candidates[i]
if(sum>target) {
sum-=candidates[i]
continue
}
path.push(candidates[i])
backcarking(i)
path.pop()
sum-=candidates[i]
}
}
backcarking(0)
return res
};
组合2 去重操作
组合2和组合1的区别在于,组合2中的元素是不可以重复选取的,比方说题目中给的例子里面存在 [1,7,1] 这三个元素,并且target为8,那么前面的1和后面的1加上7都可以等于8,但是这两个选法算是重复的,所以我们就是要对这种情况作出去重。
去重分为 树层去重 和 树枝去重 ,则分别是从递归的广度和深度来判断是否需要进行去重,举一个简单的例子
我们会发现,在深度这一方面的去重,也就是树枝去重,是没有必要的,因为题目的要求中,就存在着 1,1,6 这种选法,也就是不同的元素哪怕他们的值是相同的,也是都能够被选中。
在广度就不对了,选了第一个1之后和2组成了3,在第二个1的时候,又会和2组成3,但是这两个组合是完全相同的,并且在我们观察第一个1的递归分支以后,我们会发现,第二个1能够接触到的组合,第一个1因为值和它一样,所以也一定会接触到,那么是不是说明,在使用完第一个1去进行递归以后,第二个1就完全没有递归的必要了,所以在这我们就发现了,我们还需要对目标数组进行排序,方便我们发现前后的数字是一样的,那样的话,在首个数字递归以后,之后的就都可以跳过。
所以我们就能够发现,去重的关键点在于,数层去重
那么如何实现树层去重呢,首先我们可以在定义一个数组用于保存当前的数组状态,也就是哪一个元素被使用了,哪一个元素还没有被使用过,那么再加上上面说的,一旦排序过后,前一个元素被使用了,那么后一个相同的元素就没有必要在做相同的递归了。
function combinationSum2(candidates, target) {
candidates.sort((a,b)=>a-b)
console.log(candidates)
let res = []
let sum = 0
let path = []
let n = candidates.length
let used = new Array(n).fill(0)
const backtracking = (index) => {
if(sum>target) return
if(sum==target) {
res.push([...path])
return
}
for(let i=index;i<n;i++){
let cur = candidates[i]
if(cur > target-sum || (i>index && cur==candidates[i-1] && used[i-1] == 0)) {
continue
}
used[i] = 1
path.push(cur)
sum+=cur
backtracking(i+1)
used[i] = 0
path.pop()
sum-=cur
}
}
backtracking(0)
return res
};