39. 组合总和
思路
本题和之前的组合问题不同的地方在于对于同一个数字可以无限制重复选取。
搜索树状图如下:
注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!
对于startIndex的问题
本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合,216.组合总和III。
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合
剪枝优化
剪枝的操作是先对candidates进行排序,然后和比target大的枝就没必要遍历了。
代码
原始
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
def helper(startIndex):
if sum(path) > target:
return
if sum(path) == target:
res.append(path.copy())
return
for i in range(startIndex, len(candidates)):
path.append(candidates[i])
helper(i)
path.pop()
helper(0)
return res
剪枝后
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
candidates.sort()
total = 0
def helper(startIndex):
nonlocal total
if total == target:
res.append(path.copy())
return
for i in range(startIndex, len(candidates)):
if total + candidates[i] > target:
continue
total += candidates[i]
path.append(candidates[i])
helper(i)
path.pop()
total -= candidates[i]
helper(0)
return res
对于continue的一些细节
复杂度分析
- 时间复杂度:
O(n * 2^n)
,注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此 - 空间复杂度:
O(target)
40.组合总和II
思路
这个题目和39.组合总和的区别在于:
- 本题candidates 中的每个数字在每个组合中只能使用一次。
- 本题数组candidates的元素是有重复的,而39.组合总和 是无重复元素的数组candidates
所以这道题需要处理重复的组合。
元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
在代码里我们使用指针来去重。
代码
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
total = 0
candidates.sort()
def helper(index):
nonlocal total
if total == target:
res.append(path.copy())
return
for i in range(index, len(candidates)):
# i > index 保证每一层一个元素只用一次
if i > index and candidates[i] == candidates[i-1]:
continue
if total + candidates[i] > target:
continue
total += candidates[i]
path.append(candidates[i])
helper(i+1)
path.pop()
total -= candidates[i]
helper(0)
return res
复杂度分析
- 时间复杂度:
O(n * 2^n)
- 空间复杂度:
O(n)
131.分割回文串
思路
切割其实类似组合问题。
例如对于字符串abcdef
:
组合问题:选取一个a
之后,在bcdef
中再去选取第二个,选取b
在cdef
中再选取第三个…。
切割问题:切割一个a
之后,在bcdef
中再去切割第二段,切割b
之后在cdef
中再切割第三段…。
所以遍历的树状图如下:
优化
对于判定回文串的部分我们可以使用动态规划建表来完成,然后在我们的回溯函数中直接查询即可。
具体的建表方法可以参考
在制表的时候遍历顺序为从下到上,从左到右。这样保证dp[i + 1][j - 1]都是经过计算的。
推导dp数组
举例,输入:“aaa”,dp[i][j]状态如下:
以下是两个构建回文串二维数组的例子:
代码
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
path = []
def helper(index):
if index == len(s):
res.append(path.copy())
return
for i in range(index, len(s)):
tempStr = s[index:i+1]
if self.isPalindrome(tempStr):
path.append(tempStr)
helper(i+1)
path.pop()
helper(0)
return res
def isPalindrome(self, s):
l, r = 0, len(s)-1
while l <= r:
if s[l] != s[r]:
return False
l, r = l+1, r-1
return True
- 时间复杂度:
O(n * 2^n)
- 空间复杂度:
O(n^2)
使用dp数组记录回文子串
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
path = []
dp = [[False] * len(s) for _ in range(len(s))] # len(s) * len(s) 2d dp
self.computePalindrome(s, dp)
def helper(index):
if index == len(s):
res.append(path.copy())
return
for i in range(index, len(s)):
if dp[index][i]:
path.append(s[index:i+1])
helper(i+1)
path.pop()
helper(0)
return res
def computePalindrome(self, s, dp):
for i in range(len(s) - 1, -1, -1):
for j in range(i, len(s)):
if s[i] == s[j]:
if j - i <= 1: # 情况1 和情况2
dp[i][j] = True
elif dp[i+1][j-1]: # 情况3
dp[i][j] = True
- 时间复杂度:
O(n^2) + O(2^n) = O(2^n)
, 建造回文子串2维数组需要O(n^2)
- 空间复杂度:
O(n^2)