代码随想录算法训练营第二十七天|39.组合总和、40.组合总和II、131.分割回文串

39.组合总和

思路:

本题和77.组合 216.组合总和III的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。

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

39.组合总和

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

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

剪枝优化:

在这个树形结构中:

39.组合总和

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

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

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

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

39.组合总和1

代码:

未剪枝优化

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        result = [] # 初始化一个空列表,用于存储所有满足条件的组合
        path = [] # 初始化一个空列表,用于存储当前正在构建的组合
        self.backtracking(candidates, target, 0, 0, path, result) # 调用回溯函数开始搜索
        return result # 返回所有满足条件的组合

    def backtracking(self, candidates, target, total, startIndex, path, result):
        if total > target: # 如果当前组合的总和大于目标值,返回上一层
            return
        if total == target: # 如果当前组合的总和等于目标值,则将该组合添加到结果列表中
            result.append(path[:]) # 注意这里使用了切片操作,是为了避免直接添加path的引用,从而确保添加到result中的是组合的一个副本
            return

        for i in range(startIndex, len(candidates)): # 从startIndex开始遍历candidates数组
            total += candidates[i] # 将当前数字添加到当前组合的总和中
            path.append(candidates[i]) # 将当前数字添加到当前正在构建的组合中
            self.backtracking(candidates, target, total, i, path, result) # 递归调用回溯函数,继续搜索。startIndex不用i+1了,表示可以重复使用相同的数
            total -= candidates[i] # 回溯,将当前数字从当前组合的总和中减去
            path.pop() # 回溯,将当前数字从当前正在构建的组合中移除

剪枝优化 

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        result = []
        path = []
        candidates.sort() # 需要排序
        self.backtracking(candidates, target, 0, 0, path, result)
        return result

    def backtracking(self, candidates, target, total, startIndex, path, result):
        if total == target:
            result.append(path[:])
            return

        for i in range(startIndex, len(candidates)):
            if total + candidates[i] > target: # 剪枝操作
                break
            total += candidates[i]
            path.append(candidates[i])
            self.backtracking(candidates, target, total, i, path, result)
            total -= candidates[i]
            path.pop()
  • 时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
  • 空间复杂度: O(target)

40. 组合总和 II

思路:

本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合

一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时!所以要在搜索的过程中就去掉重复组合。

所谓去重,其实就是使用过的元素不能重复选取。

都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)强调一下,树层去重的话,需要对数组排序!

选择过程树形结构如图所示:

40.组合总和II

直接用startIndex来去重也是可以的, 就不用used数组了 

 if i > startIndex and candidates[i] == candidates[i - 1]

 可以让同一层级,不出现相同的元素(就是重复出现的只保留最左侧的一支树枝,其他的就去重)

代码:

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        result = [] # 初始化结果列表,用于存放所有可能的组合
        path = [] # 初始化当前路径列表,用于构建当前的组合
        candidates.sort() # 对候选数字列表进行排序,以便处理重复数字时能够跳过它们
        self.backtracking(candidates, target, 0, 0, path, result) # 调用回溯方法开始搜索所有可能的组合
        return result # 返回所有有效的组合列表
    
    def backtracking(self, candidates, target, total, startIndex, path, result):
        if total == target: # 如果当前路径的总和等于目标值,则将当前路径添加到结果列表中
            result.append(path[:])
            return

        for i in range(startIndex, len(candidates)): # 遍历候选数字列表,从startIndex开始
            if i > startIndex and candidates[i] == candidates[i - 1]: # 去重。如果当前数字和前一个数字相同,并且不是第一个数字,则跳过,以避免重复的组合
                continue
            if total + candidates[i] > target: # 剪枝。如果加上当前数字后的总和已经超过了目标值,则结束循环
                break
            total += candidates[i] # 将当前数字添加到当前组合的总和中
            path.append(candidates[i]) # 将当前数字添加到当前正在构建的组合中
            self.backtracking(candidates, target, total, i + 1, path, result) # 递归调用回溯函数,继续搜索。更新startIndex为i+1,确保不会重复使用相同的数字
            total -= candidates[i] # 回溯,将当前数字从当前组合的总和中减去
            path.pop() # 回溯,将当前数字从当前正在构建的组合中移除
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n)

131. 分割回文串

思路:

本题这涉及到两个关键问题:

  1. 切割问题,有不同的切割方式
  2. 判断回文

这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。 

一些同学可能想不清楚 回溯究竟是如何切割字符串呢?

我们来分析一下切割,其实切割问题类似组合问题

例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。

所以切割问题,也可以抽象为一棵树形结构,如图:

131.分割回文串

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。

 

代码:

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        result = [] # 初始化一个空列表,用于存储所有可能的回文子串组合
        path = [] # 初始化一个空列表,用于在回溯过程中存储当前的回文子串组合
        self.backtracking(s, 0, path, result) # 调用回溯方法开始搜索
        return result # 返回所有可能的回文子串组合
    
    def backtracking(self, s, start_index, path, result ):
        if start_index == len(s): # 如果已经遍历(切割)到字符串的末尾
            result.append(path[:]) # 将当前的回文子串组合添加到结果列表中(注意这里使用了path的副本,避免后续修改影响结果)
            return # 结束递归,返回上一层
        
        for i in range(start_index, len(s)): # 从start_index开始遍历字符串的剩余部分
            if s[start_index: i + 1] == s[start_index: i + 1][::-1]: # 检查从start_index到i的子串是否是回文
                path.append(s[start_index:i+1]) # 如果是回文,则将其添加到当前的回文子串组合中
                self.backtracking(s, i+1, path, result) # 递归调用,从下一个位置继续搜索。注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
                path.pop() # 回溯,移除最后一个添加的回文子串,尝试其他可能的组合

时间复杂度:O(N * 2 ^ N),因为总共有O(2^N)种分割方法,每次分割都要判断是否回文需要 O(N) 的时间复杂度。
空间复杂度:O(2 ^ N),返回结果最多有O(2 ^ N)种划分方法。

 

  • 34
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值