39. 组合总和

39. 组合总和

39. 组合总和

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

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

对于给定的输入,保证和为target的不同组合数少于150个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
23 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

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

示例 3:

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

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

思路

题目中的无限制重复被选取,吓得我赶紧想想 出现0 可咋办(如果有0,递归到0时,继续递归每次选个0,又继续递归,target不变,会陷入死循环),然后看到下面提示:2 <= candidates[i] <= 40,我就放心了。

本题和77. 组合【含回溯详解、N叉树类比、剪枝优化】以及216.组合总和III的区别是:本题没有数量要求,可以无限重复(即同一个数字可以无限次选),但是有总和的限制,所以间接的也是有个数的限制。

本题搜索的过程抽象成树形结构如下:
在这里插入图片描述

注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

而在77. 组合【含回溯详解、N叉树类比、剪枝优化】以及216.组合总和III中都可以知道要递归K层,因为要取k个元素的组合。

回溯三部曲

1.递归函数参数

首先是题目中给出的参数,集合candidates, 和目标值target

然后这里依然是定义两个变量,二维切片res存放结果集,切片path存放符合条件的结果。

此外还可以定义一个int型的sum变量来统计单一结果path里的总和,但这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了。

最后本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77. 组合【含回溯详解、N叉树类比、剪枝优化】以及216.组合总和III

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍。

代码如下:

func backtracking(candidates []int,target int,res *[][]int,path *[]int,startIndex int){}

2.递归终止条件
在如下树形结构中:

在这里插入图片描述

从叶子节点可以清晰看到,终止只有两种情况,sum大于targetsum等于target。不用sum变量的话,对应target < 0 target == 0

sum等于target(target ==0)的时候,需要收集结果,代码如下:

if target < 0 {
   return 
}
if target == 0 {
   *res = append(*res,append([]int(nil),*path...))
   return
}

3.单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合。

注意本题和77. 组合【含回溯详解、N叉树类比、剪枝优化】以及216.组合总和III的一个区别是:本题元素为可重复选取的。

如何重复选取呢,看代码,注释部分:

for i := startIndex;i < len(candidates);i++ {
    *path = append(*path,candidates[i])
    // 关键点:不用i+1了,表示下一层递归可以重复读取当前的数
    // 此外target - candidates[i]作为参数进行值传递,隐含了target的回溯
    backtracking(candidates,target - candidates[i],res,path,i) 
    *path = (*path)[0:len(*path) - 1]  // 回溯
}

按照关于回溯法模板,不难写出如下Go完整代码:

版本一

func combinationSum(candidates []int, target int) [][]int {
    if len(candidates) == 0 {
        return nil 
    }
    res := make([][]int,0)
    path := make([]int,0)
    backtracking(candidates,target,&res,&path,0)
    return res
}


func backtracking(candidates []int,target int,res *[][]int,path *[]int,startIndex int){
    if target < 0 {
        return 
    }
    if target == 0 {
        *res = append(*res,append([]int(nil),*path...))
        return
    }
    for i := startIndex;i < len(candidates);i++ {
        *path = append(*path,candidates[i])
        // 关键点:不用i+1了,表示下一层递归可以重复读取当前的数
        // 此外target - candidates[i]作为参数进行值传递,隐含了target的回溯
        backtracking(candidates,target - candidates[i],res,path,i) 
        *path = (*path)[0:len(*path) - 1]
    }
}

在这里插入图片描述

剪枝优化

在这个树形结构中:

在这里插入图片描述
以及上面的版本一的代码大家可以看到,对于sum已经大于target,即target减到小于0的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断target < 0的话就返回。

其实如果已经知道下一层的target会小于0,就没有必要进入下一层递归了。

那么可以在for循环的搜索范围上做做文章了。

对总集合排序之后,如果下一层的target(就是本层的 target - candidates[i]) 已经小于0,就可以结束本轮for循环的遍历,这是在横向剪枝,上面版本一是纵向遇到递归终止条件后才返回。

如图:

在这里插入图片描述

for循环剪枝代码如下:

    for i := startIndex;i < len(candidates) && target - candidates[i] >= 0;i++ {}

整体代码如下:(注意注释的部分)

func combinationSum(candidates []int, target int) [][]int {
    if len(candidates) == 0 {
        return nil 
    }
    res := make([][]int,0)
    path := make([]int,0)
    sort.Ints(candidates) // 排序,为了横向剪枝,不剪枝也能AC,但是效率慢点
    backtracking(candidates,target,&res,&path,0)
    return res
}


func backtracking(candidates []int,target int,res *[][]int,path *[]int,startIndex int){
    if target < 0 {
        return 
    }
    if target == 0 {
        *res = append(*res,append([]int(nil),*path...))
        return
    }
    //  target - candidates[i] >= 0为横向剪枝
    for i := startIndex;i < len(candidates) && target - candidates[i] >= 0;i++ {
        *path = append(*path,candidates[i])
        // 关键点:不用i+1了,表示下一层递归可以重复读取当前的数
        // 此外target - candidates[i]作为参数进行值传递,隐含了target的回溯
        backtracking(candidates,target - candidates[i],res,path,i) 
        *path = (*path)[0:len(*path) - 1]
    }
}

总结

本题和我们之前讲过的77.组合、216.组合总和III 有两点不同:

  • 组合没有数量要求
  • 元素可无限重复选取

针对这两个问题,我都做了详细的分析。

并且给出了对于组合问题,什么时候用startIndex,什么时候不用,并用17.电话号码的字母组合做了对比。

最后还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到。

在求和问题中,排序之后加剪枝是常见的套路!

可以看出写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值