leetcode刷题记录-回溯之组合总和 1&2

148 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第29天,点击查看活动详情

前言

今天的题目为两个中等题,一个是之前做过的就不在细说,另一个就是前一道题的变形,可以拿来和前一道题做一个对比。

每日一题

今天的题目是 40. 组合总和 II 以及 39.组合总和,难度都为中等,其中第二题前几天刚做过,今天就只是将两题的思路做一个对比,所以第二题的题目就不放了。

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] ]

提示:

  • 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是完全相同的,也就是重复了,就可以不用进行,这样后面才不用进行去重。

1927c5f29fef463acc0ab87107c7086.jpg

然后剩下的就是解题了,按照以前的回溯三部曲来进行解题。

  1. 思考清楚递归函数需要的参数 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
};

image.png

组合2 去重操作

组合2和组合1的区别在于,组合2中的元素是不可以重复选取的,比方说题目中给的例子里面存在 [1,7,1] 这三个元素,并且target为8,那么前面的1和后面的1加上7都可以等于8,但是这两个选法算是重复的,所以我们就是要对这种情况作出去重。

去重分为 树层去重 和 树枝去重 ,则分别是从递归的广度和深度来判断是否需要进行去重,举一个简单的例子

image.png

我们会发现,在深度这一方面的去重,也就是树枝去重,是没有必要的,因为题目的要求中,就存在着 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
};

image.png