Summary of Recursion.
LeetCode 77 Combinations
n
is the width of the recursion tree. k
is the depth of the recursion tree.
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
path = []
def backtrack(n, k, start):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n + 1):
path.append(i)
backtrack(n, k, i+1)
path.pop()
backtrack(n, k, 1)
return res
Prune the tree: any recursion starting from the second element in current tree level is meaningless. len(path)
is the number of element in the list at the current stage. k-len(path)
is the number of additional elements we need. k-len(path)+1
is the starting point of recursion, at most. So to prune the tree, we end for loop at k-len(path)+1
.
The time complexity is O(k* (n choose k)). It takes O((n choose k)) to enumerate combinations. And it takes O(k) to store each combination.
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
res = []
path = []
def backtrack(n, k, start):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n-(k-len(path))+1 +1):
path.append(i)
backtrack(n, k, i+1)
path.pop()
backtrack(n, k, 1)
return res
LeetCode 17 Letter Combinations of a Phone Number
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
res = []
path = ""
MAP = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
if not digits: return []
def backtrack(digits, index, path):
if len(digits) == len(path):
res.append(path[:])
return
digit = int(digits[index])
letters = MAP[digit]
for i in range(len(letters)):
path += letters[i]
backtrack(digits, index+1, path)
path = path[:-1]
backtrack(digits, 0, path)
return res
LeetCode 39 Combination Sum
Note that same number can be repeatedly chosen.
For the vertical iteration, the stop criteria of recursion is 1) sum equals to the target 2) sum is larger than the target. For the horizontal iteration, in the first layer, we have n choices, from the first number to the last number, so the number of branches is n. In the first branch, we can only select number from n numbers (starting from the first number). In the second branch, we can only select number from n-1 numbers (starting from the second number). Note that we can select the same number. This pattern stays the same for other layers until the stoping criteria is satisfied.
Time complexity (loose): O(n*2^n). For n positions of the result, we choose or not choose a value. The space complexity is O(target), depending on the depth the recursion tree.
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
SUM = 0
def backtrack(candidates, target, SUM, start):
if sum(path) > target:
return
if sum(path) == target:
res.append(path[:])
return
for i in range(start, len(candidates)):
SUM += candidates[i]
path.append(candidates[i])
backtrack(candidates, target, SUM, i)
SUM -= candidates[i]
path.pop()
backtrack(candidates, target, SUM, 0)
return res
There are two optimizations: 1] when we determine whether sum > target
, we already go into the next round of recursion. However, we don’t need to go to the next round of recursion if we determine sum + candidate[i] > target
in advance. To implement this optimization, we add one line of code to return the back track function before going into the next round. 2] Notice that if we implement (1), we have to make sure that the candidate lists is sorted, because when we stop the next recursion, we are ignoring all the values after that node.
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
SUM = 0
def backtrack(candidates, target, SUM, start):
if sum(path) == target:
return res.append(path[:])
for i in range(start, len(candidates)):
if SUM + candidates[i] > target:
return
SUM += candidates[i]
path.append(candidates[i])
backtrack(candidates, target, SUM, i) # number can repeat
SUM -= candidates[i]
path.pop()
candidates = sorted(candidates)
backtrack(candidates, target, SUM, 0)
return res
LeetCode 40 Combination Sum II
Note that each number in the candidates list can only be used once in the combination, and there could be duplicates in the candidates list. The difficulty is that there are duplicates in the list but we cannot have duplicated combinations in the result. So the key is that we remove duplicates in the same layer, and do not remove duplicates in the same branch.
Time complexity (loose): O(n*2^n). N is the length of candidates. We need O(n) time to store each combination. There are O(2^n) number of combinations.
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
res = []
path = []
SUM = 0
def backtrack(candidates, target, SUM, start):
if sum(path) == target:
return res.append(path[:])
for i in range(start, len(candidates)):
if SUM + candidates[i] > target: return
if i > start and candidates[i] == candidates[i-1]: continue
SUM += candidates[i]
path.append(candidates[i])
backtrack(candidates, target, SUM, i+1) # same number cannot be used twice
SUM -= candidates[i]
path.pop()
candidates = sorted(candidates)
backtrack(candidates, target, SUM, 0)
return res
LeetCode 216 Combination Sum III
We are going to find all combinations of length k that sum up to the target number n. The time complexity is O(k* (n choose k)).
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
res = []
path = []
def backtrack(n, k, start):
if len(path) == k:
if sum(path) == n:
res.append(path[:])
return
for i in range(start, 9-(k-len(path))+2):
path.append(i)
backtrack(n, k, i+1)
path.pop()
backtrack(n, k, 1)
return res
LeetCode 131 Palindrome Partitioning
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
path = []
def backtrack(s, start):
if start >= len(s):
res.append(path[:])
return
for i in range(start, len(s)):
temp = s[start:i+1]
if temp == temp[::-1]:
path.append(temp)
backtrack(s, i+1)
path.pop()
else:
continue
backtrack(s, 0)
return res
class Solution:
def partition(self, s: str) -> List[List[str]]:
res = []
path = []
def backtrack(s, start):
if start >= len(s):
res.append(path[:])
return
for i in range(start, len(s)):
if self.isPalindrome(s, start, i):
path.append(s[start:i+1])
backtrack(s, i+1)
path.pop()
else:
continue
backtrack(s, 0)
return res
def isPalindrome(self, s, start, end):
while start < end:
if s[start] != s[end]:
return False
start += 1
end -= 1
return True
LeetCode 93 Restore IP Addresses
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
res = []
if len(s) > 12:
return res
def backtrack(s, start, point_num):
if point_num == 3:
if self.isvalid(s, start, len(s)-1):
res.append(s[:])
return
for i in range(start, len(s)):
if self.isvalid(s, start, i):
s = s[:i+1]+"."+s[i+1:]
backtrack(s, i+2, point_num+1)
s = s[:i+1]+s[i+2:]
else:
return
backtrack(s, 0, 0)
return res
def isvalid(self, s, start, end):
if start > end:
return False
if s[start] == "0" and start != end:
return False
if not 0 <= int(s[start:end+1]) <= 255:
return False
return True
LeetCode 78 Combinations
We are going to store all nodes in the recursion tree rather than only the leaves. In the implementation, we collection path
before stopping recursion, to make sure every node is collected into the result list.
The stopping criteria is when the length of the path is equal to the length of the candidate list. So we simply iterate the nums list by using for loop and no other stopping criteria needed within the for loop.
The time complexity is O(n*2^n). There are O(2^n) combinations of a list of length n. Thinking of every number in the list can be chosen or not be chosen. It takes O(n) time to generate each list.
The space complexity is O(n), which is the space taken by path.
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
def backtrack(nums, start):
res.append(path[:])
if len(path) == len(nums): # start == len(nums)
return
for i in range(start, len(nums)):
# no stopping criteria
path.append(nums[i])
backtrack(nums, i+1)
path.pop()
backtrack(nums, 0)
return res
LeetCode 90 Subsets II
There could be duplicates in the input list, but we do not want duplicated combinations in the result list. In the same level of the recursion tree, that is the for loop in the recursion function, we need to skip the duplicates. That also requires us to sort the input list in advance so as to easily determine duplicates.
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
def backtrack(nums, start):
res.append(path[:])
if len(path) == len(nums):
return
for i in range(start, len(nums)):
# stopping criteria
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
backtrack(nums, i+1)
path.pop()
nums = sorted(nums)
backtrack(nums, 0)
return res
LeetCode 46 Permutations
The difference between permutations and combinations is that the order matters for permutations. So we need to keep track of the number used in each recursion.
The assumption of using used
list or checking whether the current number exists in the path, is that there is no duplicates in the input list.
Time complexity O(n * n!). The number of calling backtrack
function is O(n!). It takes O(n) time to store the path
result to the result list. The space complexity is O(n).
# Using `used`.
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
used = []
path = []
def backtrack(nums, used):
if len(nums) == len(path):
return res.append(path[:])
for i in range(len(nums)):
if nums[i] in used:
continue
used.append(nums[i])
path.append(nums[i])
backtrack(nums, used)
used.pop()
path.pop()
backtrack(nums, used)
return res
# Not using `used`.
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
def backtrack(nums):
if len(nums) == len(path):
return res.append(path[:])
for i in range(len(nums)):
if nums[i] in path:
continue
path.append(nums[i])
backtrack(nums)
path.pop()
backtrack(nums)
return res
LeetCode 47 Permutation II
The additional condition based on LC 46 is that: there could be duplicated numbers. So we need to prune the tree when it leads to duplicated results. We only need to remove duplicates in the for loop. We don’t want to remove duplicates in the recursion rounds. So we do the following two things:
1 We keep track of which number is actually used by used
array and skip it in the recursion, so that we make sure we are not repeatedly using any number.
2 In each layer of the recursion tree, we remove duplicates. In the implementation, within for loop, we check duplicate and skip it. Notice that all of the numbers considered in the same layer are not used.
Time complexity O(n * n!). The number of calling backtrack
function is O(n!). It takes O(n) time to store the path
result to the result list. The space complexity is O(n).
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
used = [0]*len(nums)
def backtrack(nums, used):
if len(nums) == len(path):
res.append(path[:])
return
for i in range(len(nums)):
if not used[i]:
if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
continue
used[i] = 1
path.append(nums[i])
backtrack(nums, used)
path.pop()
used[i] = 0
nums = sorted(nums)
backtrack(nums, used)
return res
LeetCode 37 Sudoku Solver
LeetCode 51 N-Queens
We use a recursion tree to decide where to put the queen for each row. The depth and the width of the tree are O(n). The stopping criteria is 1) queens in the same row, 2) queens in the same column, 3) queens in the same diagonal line.
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
if not n: return []
board = [['.']*n for _ in range(n)]
res = []
def isValid(board, row, col):
# check col
for i in range(n):
if board[i][col] == 'Q':
return False
# check upper left
i = row-1
j = col-1
while i >= 0 and j >= 0:
if board[i][j] == 'Q':
return False
i -= 1
j -= 1
# check upper right
i = row-1
j = col+1
while i >=0 and j < n:
if board[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def backtrack(board, row, n):
if row == n:
tmp_res = []
for tmp in board:
tmp_res.append("".join(tmp))
res.append(tmp_res)
return
for col in range(n):
if not isValid(board, row, col):
continue
board[row][col] = "Q"
backtrack(board, row+1, n)
board[row][col] = "."
backtrack(board, 0, n)
return res
LeetCode 491 Increasing Subsequences
Notice that we need to find all increasing subsequences rather than sort the array and generate all subsequences. In the recursion tree, the depth of the tree is the length of the input list. The stopping criteria is 1) when the length of the path is equal to the length of the input list, 2) when the new number added is larger than the final number in the path.
In the same level of the recursion tree under the same node, we skip the duplicated node, otherwise we would have duplicated combinations. A set used
is used here to skip any number that is used. It is unique for each tree layer. Note that we cannot use used = [0] * len(nums)
(see LeetCode 47) because we cannot sort the input list in advance. We are not using used
to keep track of used numbers in recursion rounds, instead, we use start
index to skip duplicated usage in recursion rounds. This is also different from LeetCode 47, where used
is used to skip duplicates in both horizontal and vertical direction of the recursion tree.
The time complexity is O(2^n * n). The time complexity for enumerating all combinations is O(2^n). The space complexity is O(2^n).
class Solution:
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
res = []
path = []
def backtrack(nums, start):
if len(path) >= 2: # store all combinations of length > 2
res.append(path[:])
if len(path) == len(nums):
return
used = set() # unique for each tree layer
for i in range(start, len(nums)):
if (path and nums[i] < path[-1]) or nums[i] in used:
continue
used.add(nums[i])
path.append(nums[i])
backtrack(nums, i+1)
path.pop()
backtrack(nums, 0)
return res
LeetCode 332 Reconstruct Itinerary
We can construct a recursion tree to decide which airport to arrive at. The stopping criteria is that the number of airport visited is equal to the number of tickets+1. The result of the path starting from the root to the leaf. Note that we don’t need to find all of the path. If we find one of them, we can immediately terminate the recursion.
All of the tickets belong to a man who departs from “JFK”, thus, the itinerary must begin with “JFK”.
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
tickets_dict = defaultdict(list)
for item in tickets:
tickets_dict[item[0]].append(item[1])
path = ["JFK"]
def backtrack(start):
if len(tickets)+1 == len(path):
return True
tickets_dict[start].sort()
for _ in tickets_dict[start]:
end = tickets_dict[start].pop(0)
path.append(end)
if backtrack(end): # terminate if find one.
return True
tickets_dict[start].append(end)
path.pop()
backtrack('JFK')
return path