Day26 算法学习|回溯算法03

题目 39. 组合总和

错误点:

本题虽然能够重复选取元素在path里,但是在res中不能有相同的集合。

在一个整数数组 candidates 中找到所有可能的目标和为 target 的组合。同一个数字可以无限制重复被选取。

题目描述

输入参数:candidates (List[int]), target (int) 输出结果:List[List[int]]

candidates 是一组整数列表,target 是一个整数目标值。返回所有可能的 candidates 中的数相加等于 target 的组合。

解题思路

树形结构:

本题使用深度优先搜索 (DFS) 和回溯的策略来求解:

  1. 创建一个空的结果列表 result 存储所有符合条件的组合,path 存储当前的组合,sum 存储当前组合的和。
  2. 遍历 candidates 中的每个元素,将其加到 sumpath 中,并递归调用回溯函数寻找所有可能的组合。
  3. 在每次递归调用后,将 candidates[i]sumpath 中移除,即进行回溯,以寻找其他可能的组合。
  4. sum 大于 target 时,停止遍历,因为无法找到符合条件的组合。
  5. sum 等于 target 时,将 path 加到 result 中,表示找到了一个符合条件的组合。

减枝操作:

代码

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]] :
        result=[]
        self.backTracking(candidates,target,0,0,[], result)
        return result

    #回溯函数
    def backTracking(self,candidates,target,sum,startIndex,path,result):
        #end logic
        if sum>target:
            return
        if sum==target:
            result.append(path[:])
            return
        #sigle layer logic
        for i in range(startIndex,len(candidates)):# 不用i+1了,表示可以重复读取当前的数
            sum+=candidates[i]
            path.append(candidates[i])
            
            self.backTracking(candidates,target,sum,i,path,result)
            #backtracking
            sum-=candidates[i]  # you should subtract candidates[i] not i
            path.pop()

复杂度

时间复杂度: O(2^N)

由于剪枝操作,实际的时间复杂度通常会比这个值低。但是在理论上,我们通常使用最坏情况的时间复杂度。

空间复杂度: O(target)。

空间复杂度主要受递归栈深度的影响,因此它的上限仍然是 O(target),这是因为在最坏的情况下,你可能需要选择多达 target 个元素才能达到 target。这种情况发生在所有 candidates 都是 1 的情况下。

额外知识点

回溯法是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认不是一个解的话(或者至少不是最后一个解),回溯法会通过在上一步进行一些变化抛弃该解,即“回溯”并且再次尝试。这个特性使得回溯法成为解决组合问题的理想选择。

题目 40组合总和II(用used数组)

题目描述:

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次,candidates 中可能存在重复元素。

解题思路:

这个问题可以通过回溯算法解决,基本思路是遍历所有可能的组合。我们从数组的开始位置开始,尝试每一种可能的组合,如果当前的组合和已经超过目标值,我们停止进一步的搜索。如果找到了一个有效的组合,我们将其添加到结果列表中。

对于数组中的重复元素,我们需要特殊处理以避免在结果集中产生重复的组合。为了实现这一点,我们首先对数组进行排序,使得相同的元素相邻。然后,在搜索的过程中,我们跳过相同的元素,以避免产生重复的组合。

树形结构:

 剪枝(层剪,枝不剪)

代码:

class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:       
        candidates.sort()
        used=[0]*len(candidates)
        res=[]
        self.backtracking(candidates, target,0,[],res,0,used)
        return res
    def backtracking(self,candidates, target,currentSum,path,res,startIndex,used):
        #减枝操作
        if currentSum>target:
            return
        if currentSum==target:
            res.append(path[:])

        #single logic
        for i in range(startIndex,len(candidates)):
            #减枝,选择没有被用过的第一个数
            if i>startIndex and candidates[i]==candidates[i-1] and used[i]==0:
                continue
            path.append(candidates[i])
            currentSum+=candidates[i]
            used[i]+=1
            #backtracking
            self.backtracking(candidates, target,currentSum,path,res,i+1,used)
            path.pop()
            currentSum-=candidates[i]
            used[i]-=1
                

复杂度:

  • 时间复杂度: O(n * 2^n),n 是候选数组的长度。
  • 空间复杂度: O(n),用于保存递归栈以及中间的路径和结果。

131 分割回文串(不懂)

知道了怎么割,但是不知道割的地方怎么表示。

每次割都会割一个子集出来,我们需要找到子集的start和end,本题的start可以先设置为0,

但是end就是切的地方可以循环表示。然后下一个回溯用end做start,当

python的string支持切片操作

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。

例如: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]

思路

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

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

例如对于字符串abcdef:

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

切割问题,也可以抽象为一棵树形结构。

class Solution:

    def partition(self, s: str) -> List[List[str]]:
        '''
        递归用于纵向遍历
        for循环用于横向遍历
        当切割线迭代至字符串末尾,说明找到一种方法
        类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线)
        '''
        result = []
        self.backtracking(s, 0, [], result)
        return result

    def backtracking(self, s, start_index, path, result ):
        # Base Case
        if start_index == len(s):
            result.append(path[:])
            return
        
        # 单层递归逻辑
        for i in range(start_index, len(s)):
            # 此次比其他组合题目多了一步判断:
            # 判断被截取的这一段子串([start_index, i])是否为回文串
            if self.is_palindrome(s, start_index, i):
                path.append(s[start_index:i+1])
                self.backtracking(s, i+1, path, result)   # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
                path.pop()             # 回溯


    def is_palindrome(self, s: str, start: int, end: int) -> bool:
        i: int = start        
        j: int = end
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True 

时间复杂度

最差情况下,即输入字符串全部由相同的字符组成,例如"aaaa...a",这种情况下,可以产生2^(n-1)种分割方式,其中n为字符串的长度。因此,总的时间复杂度为O(n*2^n),n是指判断回文和生成子串的操作。

空间复杂度

在递归过程中,需要使用一个额外的空间来保存当前的路径,即path数组,它的大小不会超过n。此外,如果计算结果的存储空间也计入空间复杂度的话,那么最坏情况下,所有的分割都会被存储下来,需要的空间是O(2^n),因此,总的空间复杂度为O(n*2^n)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值