题目 39. 组合总和
错误点:
本题虽然能够重复选取元素在path里,但是在res中不能有相同的集合。
在一个整数数组 candidates
中找到所有可能的目标和为 target
的组合。同一个数字可以无限制重复被选取。
题目描述
输入参数:candidates
(List[int]), target
(int) 输出结果:List[List[int]]
candidates
是一组整数列表,target
是一个整数目标值。返回所有可能的 candidates
中的数相加等于 target
的组合。
解题思路
树形结构:
本题使用深度优先搜索 (DFS) 和回溯的策略来求解:
- 创建一个空的结果列表
result
存储所有符合条件的组合,path
存储当前的组合,sum
存储当前组合的和。 - 遍历
candidates
中的每个元素,将其加到sum
和path
中,并递归调用回溯函数寻找所有可能的组合。 - 在每次递归调用后,将
candidates[i]
从sum
和path
中移除,即进行回溯,以寻找其他可能的组合。 - 当
sum
大于target
时,停止遍历,因为无法找到符合条件的组合。 - 当
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"] ]
思路
本题涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
例如对于字符串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)。