39.组合总和
思路:
由于元素互不重复,而且都是正整数,说明不需要考虑负数和零的情况(难度大大减低),首先将数组排序成递增数组,由于可以重复使用元素,所以每次递归都从上一次遍历的元素开始遍历递归,不从数组头部的元素开始而从上一次遍历的元素开始的原因是,在第一次遍历的时候已经遍历完了需要取用【最开始的元素】的所有情况,所以如果从最开始的元素开始,只会重复,之后遍历过的元素也是同理。每次遍历都把该元素进行加和判断是否小于target,如果符合则继续遍历,如果等于target说明找到符合的结果,压入结果数组。大于target则直接返回。
由于是排序过的递增数组,可以进行剪枝:如果加和已经大于target,后面的元素加和肯定也会大于target,此时可以直接返回。
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
self.target = target
self.result = []
candidates.sort() # 有序数组可便于剪枝
self.backtracking(candidates, 0, [], 0)
return self.result
def backtracking(self, arr, start, path, cur) -> None:
for i in range (start, len(arr)):
if cur+arr[i] < self.target:
path.append(arr[i])
self.backtracking(arr, i, path[:], cur+arr[i])
path.pop()
elif cur+arr[i] == self.target:
path.append(arr[i])
self.result.append(path[:]) # 记得加[:]
path.pop()
return
else: # 有序数组剪枝:大于target直接break
return
规范代码:
回溯剪枝(版本二)
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
result =[]
candidates.sort()
self.backtracking(candidates, target, 0, [], result)
return result
def backtracking(self, candidates, target, startIndex, path, result):
if target == 0:
result.append(path[:])
return
for i in range(startIndex, len(candidates)):
if target - candidates[i] < 0:
break
path.append(candidates[i])
self.backtracking(candidates, target - candidates[i], i, path, result)
path.pop()
40.组合总和II
思路:
自己一开始没有想出什么思路,想要套用上一题的代码,然后进行去重复元素的操作来进行修改,但后面发现始终改不对,对照解析发现是去重的条件错误。
for i in range (start, len(arr)):
if i>start and arr[i]==arr[i-1]: # start是本轮递归遍历起始点(结果路径的该位置的第一种情况),
# 如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue
continue
这部分代码的条件自己写成了
if i>0 and arr[i]==arr[i-1]:
究其原因是没完全理解去重的过程和核心:start是本轮递归遍历起始点(结果路径的该位置的第一种情况),如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue。
如果按照自己的写法,那只是将重复的元素去除了。
以下是学习的总结:
如果arr[i]==arr[i-1]且arr[i-1]的used为0,也就是本次遍历中,与本元素相同的前一个元素并未在当前的path中使用,这说明本次遍历中【选用本元素】的情况,已经在上一次【选用上一个相同元素】的遍历情况中重复了。所以当【与本元素相同的前一个元素】并未在当前的path中使用时,该情况不可以再重复,应该跳过,直接continue。
而used[i - 1] == true,说明是进入下一层递归,去下一个数。
更详细可见:
https://programmercarl.com/0040.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CII.html#%E6%80%9D%E8%B7%AF
如果candidates[i] == candidates[i - 1]
并且 used[i - 1] == false
,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
这块比较抽象,如图:
我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。
而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:
另外一种思路去重:
与上一题类似,但start是本轮递归遍历起始点(结果路径的该位置的第一种情况),如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue
代码实现如下:
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
self.target = target
self.result = []
candidates.sort()
self.backtracking(candidates, 0, [], 0)
return self.result
def backtracking(self, arr, start, path, cur) -> None:
for i in range (start, len(arr)):
if i>start and arr[i]==arr[i-1]: # start是本轮递归遍历起始点(结果路径的该位置的第一种情况),
# 如果第二个节点与start相同,说明该位置会重复出现相同的元素,直接continue
continue
if cur+arr[i] < self.target:
path.append(arr[i])
self.backtracking(arr, i+1, path[:], cur+arr[i])
path.pop()
elif cur+arr[i] == self.target:
path.append(arr[i])
self.result.append(path[:]) # 记得加[:]
path.pop()
return
else: # 有序数组剪枝:大于target直接break
return
规范代码:使用used
class Solution:
def backtracking(self, candidates, target, total, startIndex, used, path, result):
if total == target:
result.append(path[:])
return
for i in range(startIndex, len(candidates)):
# 对于相同的数字,只选择第一个未被使用的数字,跳过其他相同数字
if i > startIndex and candidates[i] == candidates[i - 1] and not used[i - 1]:
continue
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
used[i] = True
self.backtracking(candidates, target, total, i + 1, used, path, result)
used[i] = False
total -= candidates[i]
path.pop()
def combinationSum2(self, candidates, target):
used = [False] * len(candidates)
result = []
candidates.sort()
self.backtracking(candidates, target, 0, 0, used, [], result)
return result
回溯优化:
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort()
results = []
self.combinationSumHelper(candidates, target, 0, [], results)
return results
def combinationSumHelper(self, candidates, target, index, path, results):
if target == 0:
results.append(path[:])
return
for i in range(index, len(candidates)):
if i > index and candidates[i] == candidates[i - 1]:
continue
if candidates[i] > target:
break
path.append(candidates[i])
self.combinationSumHelper(candidates, target - candidates[i], i + 1, path, results)
path.pop()
131.分割回文串
思路:
自己的大致递归思路是有的,但如何分割,分割后的具体操作自己其实想的不够透彻,还是看了文字解析之后才动手做的。
判断回文字符串很简单,尤其python只需要倒序判断是否相等即可。但重点在于如何递归切割呢?其实开始下一次递归回溯的函数调用就是一次切割,只有已遍历的字符串符合回文串的条件才能进行下一次切割,当所有切割都满足条件的时候,就是一种我们要的结果。当有一个不满足,说明切割的位置不正确,继续往后切就可以了。选取切割的位置是由for来挨个尝试的,而实际进行切割的操作是由调用函数实现的,当切割的位置是字符串的末尾,说明切割的所有都正确,此时压入结果数组。
代码实现如下:
class Solution:
def partition(self, s: str) -> List[List[str]]:
self.result = []
self.backtracking(s, 0, [])
return self.result
def backtracking(self, s:str, start:int, path:List) -> None:
if start >= len(s):
self.result.append(path[:])
return
for i in range(start, len(s)):
if s[start:i+1] == s[start:i+1][::-1]:
path.append(s[start:i+1])
self.backtracking(s, i+1, path)
path.pop()
else:
continue
规范代码:
回溯+优化判定回文函数
class Solution:
def partition(self, s: str) -> List[List[str]]:
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)):
# 若反序和正序相同,意味着这是回文串
if s[start_index: i + 1] == s[start_index: i + 1][::-1]:
path.append(s[start_index:i+1])
self.backtracking(s, i+1, path, result) # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串
path.pop() # 回溯
回溯+高效判断回文子串
class Solution:
def partition(self, s: str) -> List[List[str]]:
result = []
isPalindrome = [[False] * len(s) for _ in range(len(s))] # 初始化isPalindrome矩阵
self.computePalindrome(s, isPalindrome)
self.backtracking(s, 0, [], result, isPalindrome)
return result
def backtracking(self, s, startIndex, path, result, isPalindrome):
if startIndex >= len(s):
result.append(path[:])
return
for i in range(startIndex, len(s)):
if isPalindrome[startIndex][i]: # 是回文子串
substring = s[startIndex:i + 1]
path.append(substring)
self.backtracking(s, i + 1, path, result, isPalindrome) # 寻找i+1为起始位置的子串
path.pop() # 回溯过程,弹出本次已经添加的子串
def computePalindrome(self, s, isPalindrome):
for i in range(len(s) - 1, -1, -1): # 需要倒序计算,保证在i行时,i+1行已经计算好了
for j in range(i, len(s)):
if j == i:
isPalindrome[i][j] = True
elif j - i == 1:
isPalindrome[i][j] = (s[i] == s[j])
else:
isPalindrome[i][j] = (s[i] == s[j] and isPalindrome[i+1][j-1])
回溯+使用all函数判断回文子串
class Solution:
def partition(self, s: str) -> List[List[str]]:
result = []
self.partition_helper(s, 0, [], result)
return result
def partition_helper(self, s, start_index, path, result):
if start_index == len(s):
result.append(path[:])
return
for i in range(start_index + 1, len(s) + 1):
sub = s[start_index:i]
if self.isPalindrome(sub):
path.append(sub)
self.partition_helper(s, i, path, result)
path.pop()
def isPalindrome(self, s):
return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))
回溯+使用all函数判断回文子串
关于all函数:
Python的内置函数all,它接受一个可迭代对象作为参数。如果可迭代对象中的所有元素都为True,则all函数返回True;如果有任何元素为False,则返回False。
class Solution:
def partition(self, s: str) -> List[List[str]]:
result = []
self.partition_helper(s, 0, [], result)
return result
def partition_helper(self, s, start_index, path, result):
if start_index == len(s):
result.append(path[:])
return
for i in range(start_index + 1, len(s) + 1):
sub = s[start_index:i]
if self.isPalindrome(sub):
path.append(sub)
self.partition_helper(s, i, path, result)
path.pop()
def isPalindrome(self, s):
return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))