策略
- 先找到base case
- 回溯逻辑
解析
17. Letter Combinations of a Phone Number
ref
- https://neetcode.io/problems/combinations-of-a-phone-number
- https://www.youtube.com/watch?v=0snEunUacZY
39. Combination Sum
-
we need to avoid duplicates, e.g., [2,2,3] and [3,2,2] are the same
- how to achieve:
- in our decision tree, one branch is to include the current element ([2,2]), and another branch is not to include the current element ([2])
- i.e., 可不可以在搜索的时候就去重呢?答案是可以的。做法:遇到这一类相同元素不计算顺序的问题,我们在搜索的时候就需要 按某种顺序搜索。具体的做法是:每一次搜索的时候自增 下一轮搜索的起点 begin
- how to achieve:
-
Base case:
-
total == target
-> found a combination -
begin >= len(candidates) or total > target
-> cannot find any combination
-
-
复杂度分析
- 时间复杂度:$O(2^{target/m})$,m is the mim value in nums, target/m 是树的深度
- 空间复杂度:O(target/m)。除答案数组外,空间复杂度取决于递归的栈深度,在最差情况下需要递归 O(target/m)层。
# backtracking
# - time: O(2^(target/m)), m is the mim value in nums, target/m 是树的深度
# - space: O(target/m), m is the mim value in nums, O(target/m) 是递归时栈使用的空间(栈深度),target/m 是树的深度
def combinationSum(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
res = []
def dfs(start, curSequence, curSum):
# only consider [start, len(candidates)) from candidates
if curSum == target: # find a combination
res.append(curSequence[:]) # couldn't directly use curSequence, since we still use curSequence later
return
if curSum > target or start >= len(candidates): # cannot find any combination
return
# case 1: 包括当前 start 指向的元素
curSequence.append(candidates[start])
dfs(start, curSequence, curSum + candidates[start])
curSequence.pop(-1)
# case 2: 不包括 start 指向的元素,且 start 自增
dfs(start + 1, curSequence, curSum)
dfs(0, [], 0)
return res
Ref
- https://www.youtube.com/watch?v=GBKI9VSKdGg
- https://neetcode.io/problems/combination-target-sum
- https://leetcode.cn/problems/combination-sum/solutions/14697/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/
- https://leetcode.cn/problems/combination-sum/solutions/406516/zu-he-zong-he-by-leetcode-solution/
40. Combination Sum II
- 和 3 sum 题目的去重逻辑非常像:
- 对 candidates 先排序,变成 [1,1,2,5,6,7,10]
- 如果当前元素和前一个元素值相等,
start += 1
,直到不满足该条件
# backtracking - myself
# - time: O(n*2^n)
# - space: O(n)
def combinationSum2(self, candidates, target):
"""
:type candidates: List[int]
:type target: int
:rtype: List[List[int]]
"""
res = []
candidates.sort()
def dfs(start, subArray, curSum):
if curSum == target:
res.append(subArray[:])
return
if start >= len(candidates) or curSum > target:
return
# include candidates[start]
subArray.append(candidates[start])
# "start + 1", since [Each number in candidates may only be used once in the combination.]
dfs(start + 1, subArray, curSum + candidates[start])
# skip candidates[start] and also the candidates with the same value of candidates[start]
subArray.pop(-1)
start += 1
while start <= len(candidates) - 1 and candidates[start] == candidates[start - 1]:
start += 1
dfs(start, subArray, curSum)
dfs(0, [], 0)
return res
ref
46. Permutations
思路
- permutations的总数量:有 n 个位置,第一个位置可以选 n 个选项,第二个位置可以选 n-1 个选项,最后一个位置可以选 1 个选项,所以总共有 n * (n-1) * (n-2) *… *1 = n!
- 想如何把这个大问题化解成一个小问题
- base case 是如果只有一个元素,则直接深拷贝后返回
复杂度分析
-
时间复杂度:$O(n*n!)$,其中 n 为序列的长度。
- 时间复杂度和数组排列的方案数成线性关系,方案数为 $N×(N−1)×(N−2)…×2×1$,即复杂度为 $O(N!)$ ;数组旋转操作 使用 $O(N)$ ;因此总体时间复杂度为 O(N!N) 。
-
空间复杂度:O(n*n!),其中 n 为序列的长度。
- 递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,这里可知递归调用深度为 O(n)。
- 答案数组 需要 O(n*n!)
# backtracking - myself (逻辑有问题,虽然提交后,是完全通过的)
# - time: O(n!*n), n! = n * (n-1) * (n-2) ... * 1
# - 算法的复杂度首先受 backtrack 的调用次数制约,permute 的调用次数为 n! + n!/1! + n!/2! + n!/3! ... + n!/(n-1)! < 2n! + n!/2 + n!/2^2 + n!/2^{n-2} < 3n!
# - space: O(n!*n), O(n!*n) for the output list, n for calling stack (for recursion)
def permute(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
res = []
# base case
if len(nums) == 1:
return [nums[:]]
for _ in range(len(nums)): # run n times, time: O(n)
n = nums.pop(0) # remove the first element from nums
perms = self.permute(nums) # time: O(n!)
for perm in perms: # time: O(n), e.g., we have [2, 3], [3, 2]. Then we are gonna add 1 into them
perm.append(n)
res.extend(perms)
nums.append(n) # append the removed element to the end
return res
def permute(self, nums: List[int]) -> List[List[int]]:
self.res = []
self.backtrack(nums, 0)
return self.res
def backtrack(self, nums: List[int], idx: int):
if idx == len(nums):
self.res.append(nums[:])
return
for i in range(idx, len(nums)):
nums[idx], nums[i] = nums[i], nums[idx]
self.backtrack(nums, idx + 1)
nums[idx], nums[i] = nums[i], nums[idx]
ref
- https://www.youtube.com/watch?v=FZe0UqISmUw
https://www.youtube.com/watch?v=s7AvT7cGdSo- https://neetcode.io/problems/permutations
- https://leetcode.cn/problems/permutations/solutions/218275/quan-pai-lie-by-leetcode-solution-2/
- https://leetcode.cn/problems/permutations/solutions/2363882/46-quan-pai-lie-hui-su-qing-xi-tu-jie-by-6o7h/
- https://leetcode.cn/problems/permutations/solutions/9914/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
78. Subsets
-
遍历数组,对于数组中的每一个整数,每一步都向输出子集中所有子集添加(或不添加)这个整数,并生成新的子集
- 记原序列中元素的总数为 n。原序列中的每个数字 ai 的状态可能有两种,即「在子集中」和「不在子集中」
-
复杂度分析
- 时间复杂度:$O(n×2^n)$)。一共 $2^n$ 个状态,每种状态需要 O(n) 的时间来构造子集。
- 在整个递归调用的过程中,start 是从小到大递增的,当 start 增加到 n 的时候,记录答案并终止递归。可以看出进行枚举的时间复杂度是 $O(2^n)$
- 空间复杂度:O(n),不考虑返回结果所占用的空间。临时数组 t 的空间代价是 O(n),递归时栈空间的代价为 O(n)。
- 时间复杂度:$O(n×2^n)$)。一共 $2^n$ 个状态,每种状态需要 O(n) 的时间来构造子集。
# solution 1 - brute force (recursion)
# - time: O(n*2^n)??
# - space: O(n)
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
def backtracking(start, curSubsets):
# to consider the elements in nums: [start, len(nums))
if start >= len(nums):
res.append(curSubsets.copy())
return
# case 1: decision to include nums[i]
curSubsets.append(nums[start])
backtracking(start + 1, curSubsets)
# case 2: decision not to include nums[i]
curSubsets.pop(-1)
backtracking(start + 1, curSubsets)
backtracking(0, [])
return res
ref
- https://www.youtube.com/watch?v=REOH22Xwdkk
- https://neetcode.io/problems/subsets
- https://leetcode.cn/problems/subsets/solutions/420294/zi-ji-by-leetcode-solution/
90. Subsets II
Ref
79. Word Search
ref
131. Palindrome Partitioning
ref