算法训练Day27 | LeetCode39. 组合总和 (元素可重复,数组长度不限);40. 组合总和III(去重);131.分割回文串

目录

LeetCode39. 组合总和

1. 思路

2. 代码实现

3. 剪枝优化

4. 复杂度分析

5. 思考与收获

LeetCode40. 组合总和III

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode131.分割回文串 

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


LeetCode39. 组合总和

 链接:39. 组合总和 - 力扣(LeetCode)

1. 思路

本题和 组合,组合III的两个不同点是:

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

关于题目条件的思考

  1. 题目中的无限制重复被选取,吓得我赶紧想想 出现0 可咋办,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了;
  2. 不用考虑去重问题,因为数组中的每个元素都互不相同

本题虽然说是组合没有数量要求,元素也可以无限重复,但是有总和的限制,所以间接地也是有元素个数的限制;

本题搜索的过程抽象成树形结构如下:

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

而在77.组合216.组合总和III中都可以知道要递归K层,因为要取k个元素的组合

为什么取5的下面是在【5,3】中取,而不是【2,5,3】?

因为组合不强调元素顺序,如果这样写的话会出现重复的组合,比如【2,3】和【3,2】;

2. 代码实现

回溯三部曲

2.1 递归函数参数

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量也可以作为函数参数传入,但这里不这么做);

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

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

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

  • 如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 ,216.组合总和III ;

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

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

class Solution:
    def __init__(self):
        self.path = []
        self.paths = []

    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        '''
		def backtracking(self, candidates: List[int], target: int, sum_: int, start_index: int) -> None:

2.2 递归终止条件

在以上树形结构中,从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target;sum等于target的时候,需要收集结果,代码如下:

# Base Case
if sum_ == target:
		# 因为是shallow copy,所以不能直接传入self.path
    self.paths.append(self.path[:]) 
    return
if sum_ > target:
    return 

2.3 单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合。

注意本题和77.组合 ,216.组合总和III 的一个区别是:本题元素为可重复选取的;如何重复选取呢?在向下一层递归函数的时候,不写i+1了,直接写i,表示一个重复读取当前的数;

# 单层递归逻辑 
for i in range(start_index, len(candidates)):
    sum_ += candidates[i]
    self.path.append(candidates[i])
    # 因为无限制重复选取,所以不是i+1
    self.backtracking(candidates, target, sum_, i)  
    sum_ -= candidates[i]   # 回溯
    self.path.pop()        # 回溯

2.4 整体代码如下

class Solution(object):
    def __init__(self):
        self.path = []
        self.result = []

    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        :rtype: List[List[int]]
        """
        if candidates == []: return []
        self.traversal(candidates,target,0,0)
        return self.result
    
    def traversal(self,candidates,target,curSum,startIndex):
        # 终止条件
        if curSum == target:
            # 因为是shallow copy,所以不能直接传入self.path
            self.result.append(self.path[:])
            return 
        if curSum > target:
            return 
        # 单层回溯搜索
        for i in range(startIndex,len(candidates)):
            self.path.append(candidates[i])
            curSum += candidates[i]
            # 因为无限制重复选取,所以不是i+1
            self.traversal(candidates,target,curSum,i)
            self.path.pop()
            curSum -= candidates[i]

3. 剪枝优化

在这个树形结构中:

以及上面的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了;那么可以在for循环的搜索范围上做做文章了。对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历;

for循环剪枝代码如下:

# 别忘了在主函数里面先排序
for i in range(start_index, len(candidates)):
    if sum_ + candidates[i] > target: 
        return

剪枝之后整体代码如下:

class Solution(object):
    def __init__(self):
        self.path = []
        self.result = []

    def combinationSum(self, candidates, target):
        """
        :type candidates: List[int]
        :type target: int
        
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值