39. 组合总和
代码随想录:39. 组合总和
Leetcode:39. 组合总和
做题
无思路。
看文章
看了下思路,思路图如下:
自己动手实现了一下,AC代码如下:
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
self.res = []
self.path = []
candidates.sort()
self.size = len(candidates)
self.backtracking(candidates, target, 0, 0)
return self.res
def backtracking(self, candidates, target, cur, start):
if cur > target:
return
if cur == target:
self.res.append(self.path[:])
return
for i in range(start, self.size):
cur += candidates[i]
if cur > target:
cur -= candidates[i]
break
self.path.append(candidates[i])
self.backtracking(candidates, target, cur, i)
cur -= candidates[i]
self.path.pop()
时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
空间复杂度: O(target),大概理解为:假如target=100,那么最多就是100个1相加,即需要100个空间(bit?)
40.组合总和II
代码随想录:40.组合总和II
Leetcode:40.组合总和II
做题
不会去重。
看文章
所谓去重,其实就是使用过的元素不能重复选取。
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
因此,需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。此时for循环里就应该做continue的操作。
在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
使用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
其实,用 i 和 start 的关系就能去重,具体代码如下:
class Solution:
def backtracking(self, candidates, target, total, startIndex, 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]:
continue
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
self.backtracking(candidates, target, total, i + 1, path, result)
total -= candidates[i]
path.pop()
def combinationSum2(self, candidates, target):
result = []
candidates.sort()
self.backtracking(candidates, target, 0, 0, [], result)
return result
理解起来,还是遵循同一思路:在同一树层内不选重复的。
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
131.分割回文串
代码随想录:131.分割回文串
Leetcode:131.分割回文串
做题
无思路。
看文章
本题这涉及到两个关键问题:
- 切割问题,有不同的切割方式
- 判断回文
切割问题类似组合问题。例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个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
时间复杂度: O(n * 2^n)
空间复杂度: O(n^2)
更简洁的回文串判断方法(python):
if s[start_index: i + 1] == s[start_index: i + 1][::-1]:
或用all函数:
def isPalindrome(self, s): # s为子串
return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2))
用动态规划优化回文串判断
例如给定字符串"abcde", 在已知"bcd"不是回文字串时, 不再需要去双指针操作"abcde"而可以直接判定它一定不是回文字串。具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]且s[1:n-1]是回文字串。
如果熟悉动态规划这种算法的话, 可以高效地事先一次性计算出, 针对一个字符串s, 它的任何子串是否是回文字串, 然后在回溯函数中直接查询即可, 省去了双指针移动判定这一步骤。
具体代码如下:
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])
以往忽略的知识点小结
- 组合问题用回溯
- 组合问题去重:同一树层不取重复值
- 切割问题用回溯
- 回文判断
个人体会
完成时间:3h。
心得:题比较难,还需要熟悉什么时候、如何使用回溯,如何去重、如何切割更是难题。另外,还要学一下动态规划的思路来提前计算。