LeetCode: Backtracking
Backtracking intro
- Cheat sheet
- What is Backtracking
- Backtracking Characteristics
- Backtracking Representation
- Backtracking IRL
- Backtracking Application: DFS Generate All Combinations Or Subsets
- Backtracking Application: DFS Generate While Constraint Satisfaction
- Backtracking Application: Path Finding In Search Space
- Backtracking Application: Combinatorial Optimization
- Backtracking Application: Decision Trees And Partitioning
113. Path Sum II ::2:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Backtracking] [DFS] Recursive DFS Pre Order Backtracking Path Collection - Tree/DFS Pre Order Recursive One Sided Top Down Backtracking
- Solution 2: [Backtracking] [BFS] Iterative BFS Pre Order Per Node Path Copy Queue - Tree/BFS Pre Order Across Level No Explicit Level Sized Grouping Full Across Top Down
78. Subsets ::3:: - Medium
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Backtracking] [DFS] 2 Choices Include Integer Path Or Exclude And Continue Exploration - Backtracking/Generate All Combinations Or Subsets
- Solution 2: [Backtracking] [DFS] Yield Generator For Streaming To Avoid O(n * 2^n) Storage [SC Opt] - Backtracking/Generate All Combinations Or Subsets
1863. Sum of All Subset XOR Totals ::1:: - Easy
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Backtracking] [DFS] 2 Choices Include Integer Path Or Exclude And Continue Exploration With Final XOR - Backtracking/Generate All Combinations Or Subsets
- Solution 2: [Bit Manipulation] OR Then Shift Bit Contribution Counting - Bit Manipulation/Counting Set Bit Contributions Across All Subsets
22. Generate Parentheses ::4:: - Medium
- Intro
- Abstract
- Pseudocode
- Solution 1: [Backtracking] [DFS] Recursive with Mutable List Appending - Stack/Backtracking by Tracking History or State
- Solution 2: [Dynamic Programming] Two Pointer Opposite Ends Catalan Pattern To Build Parentheses Combinations - Stack/Dynamic Programming State Compression
51. N Queens ::2:: - Hard
- Intro
- Abstraction
- Pseudocode
- Solution 1: [Backtracking] [DFS] Recursive DFS Backtracking on Current Row with Column/Diagonal Tracking - Backtracking/Combinatorial Optimization
- Solution 2: [Backtracking] [DFS] Recursive DFS Backtracking Using Bitmasking for Columns/Diagonals - Backtracking/Combinatorial Optimization
Backtracking intro
LeetCode problems with backtracking solutions.
Cheat sheet
Backtracking is inherently dfs or bfs.
What is Backtracking
Backtracking is a systematic technique for exploring all possible solutions to a problem by building them incrementally and abandoning ('backtracking') as soon as it becomes clear that a candidate cannot lead to a valid solution.
It is often implemented recursively, but can also be simulated iteratively with a stack.
While backtracking often has exponential time complexity in the worst case, it is the most practical way to solve problems that require exploring many possibilities with pruning.
Backtracking Characteristics
- Build -> partial solution
- Prune -> remove branches that violate constraints
- Explore -> recurse or continue to extend the current partial solution
- Backtrack -> undo the last step and try another option
Backtracking Representation
-
Tree representation: Each node represents a decision state
-
Children: represent choices from the current states
-
Leaves: Either full solutions or dead nodes
Generating subsets of [1,2,3] forms a decision tree where at each step, we decide to include or exclude a number
Decision tree for [1,2,3]:
[]
/ \
[1] []
/ \ / \
[1,2] [1] [2] []
/ \ ... ...
[1,2,3] ...Backtracking IRL
In the context of solving a maze, at each intersection, you choose a path (a decision). If the path leads to a dead end, you backtrack to the intersection and try another route. Eventually, you either find the exit (solution) or exhaust all paths (no solutions).
Backtracking Application: DFS Generate All Combinations Or Subsets
Traversal Order: Root -> Choices Mindset: Process the current subset as soon as you build it, then explore further elements Trick: Ill record the current subset first, then decide which elements to include next. We can explore all subsets, permutations, or combinations by recursively building solutions and backtracking when needed.
Ex: Generate all subsets of a set
def subsets(nums):
res = []
def dfs_backtrack(start, path):
# Process Root -> : record current subset first
res.append(path[:])
# Process -> Choices : decide which to include/exclude
for i in range(start, len(nums)):
# Build: include nums[i]
path.append(nums[i])
# Explore: recurse to next index
dfs_backtrack(i + 1, path)
# Backtrack: remove last element
path.pop()
dfs_backtrack(0, [])
return res
# Example: subsets([1,2,3]) -> [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]Backtracking Application: DFS Generate While Constraint Satisfaction
Traversal Order: Root -> Choices Mindset: Build sequences step by step, only adding valid elements and backtracking when constraints are violated Tricks: Ill try to add '(' or ')' next only if rules allow, then undo if needed. As we explore all permutations, we can enforce rules in order to prune invalid branches early.
Ex: Generate all valid parentheses
def generateParenthesis(n):
res = []
def dfs_backtrack(open_count, close_count, path):
# Process Root -> : check if sequence complete
if len(path) == 2 * n:
res.append("".join(path))
return
# Process -> Choices : add '(' if possible
if open_count < n:
# Build
path.append("(")
# Explore
dfs_backtrack(open_count + 1, close_count, path)
# Backtrack
path.pop()
# Process -> Choices : add ')' if it will not break validity
if close_count < open_count:
# Build
path.append(")")
# Explore
dfs_backtrack(open_count, close_count + 1, path)
# Backtrack
path.pop()
dfs_backtrack(0, 0, [])
return res
# Example: generateParenthesis(3) -> ["((()))","(()())","(())()","()(())","()()()"]Backtracking Application: Path Finding In Search Space
Traversal Order: Root -> Choices (neighboring paths) Mindset: Explore each path step by step, backtracking when reaching dead ends. Trick: Ill walk one direction fully before trying another, undoing my steps if blocked. We can explore fully explore paths, by stepping through and backtracking when reaching dead ends for different search spaces (grids, graphs, networks).
Ex: Word Search in grid
def exist(board, word):
rows, cols = len(board), len(board[0])
def dfs_backtrack(r, c, idx):
# Early exit:
# Process Root -> : matched full word
if idx == len(word):
return True
# Early Pruning -> : check boundaries
if r < 0 or c < 0 or r >= rows or c >= cols:
return False
# Early Pruning -> : check current cell
if board[r][c] != word[idx]:
return False
# Process -> Choices : explore all 4 directions
# Build: mark current cell as exploring
tmp, board[r][c] = board[r][c], "#"
# Explore: recurse to neighbor cell
found = (dfs_backtrack(r+1, c, idx+1) or
dfs_backtrack(r-1, c, idx+1) or
dfs_backtrack(r, c+1, idx+1) or
dfs_backtrack(r, c-1, idx+1))
# Backtrack: Restore cell
board[r][c] = tmp
return found
# dfs_backtrack starting from all cells
for r in range(rows):
for c in range(cols):
if dfs_backtrack(r, c, 0):
return True
return False
# Example: exist([["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], "ABCCED") -> TrueBacktracking Application: Combinatorial Optimization
Traversal Order: Root -> Choices (column positions) Mindset: Place one queen at a time, pruning invalid columns, and backtrack when stuck Trick: Ill place a queen, explore further rows, then remove it if it leads to a conflict. We can search for an optimal solution while pruning bad candidates.
Ex: N Queens Problem
def solveNQueens(n):
res = []
cols = set()
diag1 = set() # r - c
diag2 = set() # r + c
board = [["."] * n for _ in range(n)]
def dfs_backtrack(r):
# Process Root -> : all queens placed
if r == n:
res.append(["".join(row) for row in board])
return
# Process -> Choices : try each column
for c in range(n):
if c in cols or (r-c) in diag1 or (r+c) in diag2:
continue
# Build: place queen
board[r][c] = "Q"
cols.add(c); diag1.add(r-c); diag2.add(r+c)
# Explore: recurse to next row
dfs_backtrack(r + 1)
# Backtrack: remove queen
board[r][c] = "."
cols.remove(c); diag1.remove(r-c); diag2.remove(r+c)
dfs_backtrack(0)
return res
# Example: solveNQueens(4) -> [
# [".Q..","...Q","Q...","..Q."],
# ["..Q.","Q...","...Q",".Q.."]
# ]
Backtracking Application: Decision Trees And Partitioning
Traversal Order: Root -> Choices (substring partitions) Mindset: Partition strings step by step, backtracking when a substring is not a palindrome Trick: Ill add a palindromic piece, explore further, then remove it if it doesn't lead to a solution. We can make binary or k-ary decision at each step, exploring all outcomes
Ex: Partition a string into palindromic substrings
def partition(s):
res = []
def is_palindrome(sub):
return sub == sub[::-1]
def dfs_backtrack(start, path):
# Process Root -> : reached end of string
if start == len(s):
res.append(path[:])
return
# Process -> Choices : try all possible substrings
for end in range(start+1, len(s)+1):
# Constraint check: palindrome check
if is_palindrome(s[start:end]):
# Build: add substring
path.append(s[start:end])
# Explore: recurse to next string
dfs_backtrack(end, path)
# Backtrack: remove substring
path.pop()
dfs_backtrack(0, [])
return res
# Example: partition("aab") -> [["a","a","b"], ["aa","b"]]3483. Unique 3-Digit Even Numbers ::1:: - Easy
Topics: Backtracking, Depth First Search, Array, Hash Table, Recursion, Enumeration
Intro
You are given an array of digits called digits. Your task is to determine the number of distinct three-digit even numbers that can be formed using these digits. Note: Each copy of a digit can only be used once per number, and there may not be leading zeros.
| Example Input | Output |
|---|---|
| digits = [1,2,3,4] | 12 |
| digits = [0,2,2] | 2 |
| digits = [6,6,6] | 1 |
| digits = [1,3,5] | 0 |
Constraints:
3 ≤ digits.length ≤ 10
0 ≤ digits[i] ≤ 9
Abstraction
Find all even combinations of 3 digits numbers using array
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Digit Value Recursion Backtracking Over Positions And Num Count - Tree/DFS Recursive Backtracking
def totalNumbers(self, digits: List[int]) -> int:
# Backtracking:
# Recursive function will build a 3-digit number position by position
# (hundreds -> tens -> ones),
# trying every digit VALUE 0-9 at each position
# rather than every INDEX in `digits`.
# This keeps branching factor fixed at 10 regardless of len(digits),
# so the recursion tree size is a hard constant, not dependent on n.
# Digit Counts (as given):
# We track how many copies of each value remain,
# so duplicate digits (like the two 2's) are handled naturally
# without needing to dedupe by index.
# sc: O(1)
count = [0] * 10
for d in digits:
count[d] += 1
# bounded by 450 possible distinct 3 digit even numbers
# sc: O(1)
seen = set()
# Holds current 3 digit number during recursion, max length 3
# sc: O(1)
current = []
def backtrack(position):
# New Valid number:
# filled hundreds/tens/ones, record the assembled number
# tc: O(1)
if position == 3:
num = current[0] * 100 + current[1] * 10 + current[2]
seen.add(num)
return
# Position 0 (hundreds):
# - no leading zero allowed, so start at 1
start = 1 if position == 0 else 0
# Position 2 (ones):
# - right most num must be even, so step by 2
step = 2 if position == 2 else 1
# Path 1:
# try every valid digit value at this position
for val in range(start, 10, step):
# skip if no copies of this digit value remain
if count[val] == 0:
continue
# Choose:
# use one copy of val at this position
count[val] -= 1
current.append(val)
# Explore:
# recursively fill the next position
backtrack(position + 1)
# Backtrack:
# restore the copy so sibling branches can reuse it
current.pop()
count[val] += 1
# Start backtracking at position 0, the hundreds digit
backtrack(0)
# overall: tc O(1), recursion bounded by fixed branching (<=10) and
# fixed depth (3), so at most ~9*10*5 = 450 leaf numbers explored
# overall: sc O(1), count array, recursion stack, current, and seen
# are all bounded by constants independent of len(digits)
return len(seen)17. Letter Combinations of a Phone Number ::1:: - Medium
Topics: Hash Table, String, Backtracking, Depth First Search
Intro
Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order. A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.
| Example Input | Output |
|---|---|
| digits = "23" | ["ad","ae","af","bd","be","bf","cd","ce","cf"] |
| digits = "" | [] |
| digits = "2" | ["a","b","c"] |
Constraints:
0 ≤ digits.length ≤ 4
digits[i] is a digit in the range ['2', '9'].
Abstraction
Given a mini phone number, find all the possible letter combinations.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive DFS Backtracking on Current Letter Path - Backtracking/Generate All Combinations
def letterCombinations(self, digits: str) -> List[str]:
# Backtracking:
# Recursive function will build letter combinations incrementally,
# by using DFS to try every letter mapped to the current digit,
# one digit position at a time.
# Path 1:
# empty input has no digits to map, no combinations possible
if not digits:
return []
# sc: O(1), fixed mapping of 8 digits to their letters
digit_map = {
"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"
}
# sc: O(4^n * n) worst case, up to 4^n combinations each of length n
res = []
# sc: O(n), holds current combination during recursion, max length n = len(digits)
path = []
def dfs(index):
# New Valid combination:
# path length matches digits length, every digit has been mapped
# tc: O(n) to join path into a string
if index == len(digits):
res.append("".join(path))
return
# Path 2:
# try every letter mapped to the digit at this position
# tc: O(4) per call, bounded by max letters per digit (7 and 9 map to 4)
for char in digit_map[digits[index]]:
# Choose:
# add letter to path
path.append(char)
# Explore:
# recurse to the next digit position
dfs(index + 1)
# Backtrack:
# remove last letter to try the next option via the for loop
path.pop()
# Start backtracking at "root"
dfs(0)
# overall: tc O(4^n * n), n = len(digits), up to 4 choices per digit, n chars to join per combination
# overall: sc O(n), recursion stack + path (auxiliary, excluding output)
return res797. All Paths From Source To Target ::2:: - Medium
Topics: Backtracking, Depth First Search, Breadth First Search, Graph Theory, Adjacency List, Directed Acyclic Graph
Intro
Given a directed acyclic graph (DAG) of n nodes labeled from 0 to n - 1, find all possible paths from node 0 to node n - 1 and return them in any order. The graph is given as follows: graph[i] is a list of all nodes you can visit from node i (i.e., there is a directed edge from node i to node graph[i][j]).
| Example Input | Output |
|---|---|
| graph = [[1,2],[3],[3],[]] | [[0,1,3],[0,2,3]] |
| graph = [[4,3,1],[3,2,4],[3],[4],[]] | [[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]] |
Constraints:
n == graph.length
2 ≤ n ≤ 15
0 ≤ graph[i][k] < n
graph[i][j] != i (i.e., there will be no self-loops).
All the elements of graph[i] are unique.
The input graph is guaranteed to be a DAG.
Abstraction
Find all possible paths from node to node.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] DFS Backtracking Path Building - Tree/DFS Recursive Backtracking
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
# Backtracking:
# Recursive function will build paths from node 0 to node n-1
# incrementally, by using DFS to extend the current path along every
# outgoing edge. Since the graph is guaranteed to be a DAG, there are
# no cycles to worry about, so no visited set is needed -- every path
# DFS explores is guaranteed to terminate.
# Adjacency List (as given):
# graph = [
# [1, 2], 0 -> 1, 0 -> 2
# [3], 1 -> 3
# [3], 2 -> 3
# [], 3 has no outgoing edges
# ]
# Unlike most graph problems, the input here is already an Adjacency
# List -- graph[i] is literally the list of nodes reachable from i.
# There's no Edge List to transform first.
n = len(graph)
# sc: O(2^V * V) worst case, DAG can have exponentially many paths
res = []
# sc: O(V), holds current path during recursion, max length V
path = [0]
def dfs(node):
# New Valid path:
# reached the target, record a copy of the current path
# tc: O(V) to copy path
if node == n - 1:
res.append(path[:])
return
# Path 1:
# try extending the path along every outgoing edge from this node
for nei in graph[node]:
# Choose:
# extend path with neighbor
path.append(nei)
# Explore:
# recursively explore neighbor
dfs(nei)
# Backtrack:
# remove neighbor so sibling branches can reuse path
path.pop()
# Start backtracking at "root", node 0
dfs(0)
# overall: tc O(2^V * V), up to 2^V paths in the worst case DAG, each up to V nodes to copy
# overall: sc O(2^V * V), for storing all paths in res
# (auxiliary space, excluding output, is O(V) for recursion stack + path)
return resSolution 2: [Backtracking] [BFS] BFS Iterative Path Building - Tree/BFS Iterative Backtracking
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
# Backtracking (Iterative):
# Same idea as Solution 1 -- build paths from node 0 to node n-1 by
# extending along every outgoing edge -- but explored via a queue
# instead of recursion. Since BFS has no call stack to backtrack on,
# each queue entry must carry its OWN independent copy of the path
# so far -- there's no shared mutable list to undo after a node.
# Unlike single-target BFS problems, we can't stop at the first time
# the target is reached, since every path is needed, not just the
# shortest one. Every time a path reaches the target it's recorded,
# but BFS continues until the queue is empty so all other
# in-progress paths are still explored.
n = len(graph)
# sc: O(2^V * V) worst case, DAG can have exponentially many paths
res = []
# sc: O(2^V * V), queue can hold many in-progress paths at once,
# each up to O(V) long, with its own independent copy
queue = deque([[0]])
while queue:
# Pop a path from the queue
path = queue.popleft()
# Grab the last node in this path to explore its neighbors
node = path[-1]
# New Valid path:
# reached the target, record this complete path
if node == n - 1:
res.append(path)
continue
# Path 1:
# try extending the path along every outgoing edge from this node
for nei in graph[node]:
# Choose + Explore:
# build a new path with neighbor appended, queue for further exploration
# tc: O(V) per enqueue to copy and extend path
queue.append(path + [nei])
# overall: tc O(2^V * V), up to 2^V paths in the worst case DAG, each up to V nodes to build/copy
# overall: sc O(2^V * V), for storing all paths in the queue and res
return res113. Path Sum II ::2:: - Medium
Topics: Backtracking, Tree, Depth First Search, Breadth First Search, Binary Tree
Intro
Given the root of a binary tree and an integer targetSum, return all root-to-leaf paths where the sum of the node values in the path equals targetSum. Each path should be returned as a list of the node values, not node references. A root-to-leaf path is a path starting from the root and ending at any leaf node. A leaf is a node with no children.
| Example Input | Output |
|---|---|
| root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22 | [[5,4,11,2],[5,8,4,5]] |
| root = [1,2,3], targetSum = 5 | [] |
| root = [1,2], targetSum = 0 | [] |
Constraints:
The number of nodes in the tree is in the range [0, 5000].
-1000 ≤ Node.val ≤ 1000
-1000 ≤ targetSum ≤ 1000
Abstraction
Check if a root to leaf path exists that adds to target and return all possible paths.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive DFS Pre Order Backtracking Path Collection - Tree/DFS Pre Order Recursive One Sided Top Down Backtracking
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
# Backtracking:
# Recursive function will trace root-to-leaf paths incrementally,
# by using DFS to descend into every subtree while tracking the
# running path and remaining sum still needed. Unlike a problem
# that can stop at the first valid path, every root-to-leaf path
# must be explored, and any that sum to targetSum must be collected.
# sc: O(n) worst case (skewed tree), holds all collected paths
res = []
# sc: O(h), holds current path during recursion, max length = tree height h
path = []
def dfs(node, remaining):
# Path 1:
# reached past a leaf, empty subtree cannot complete a path
if not node:
return
# Choose:
# add this node to the current path before descending
path.append(node.val)
remaining -= node.val
# New Valid path:
# true leaf -- no left AND no right child -- record this path
# only if it exactly accounts for the remaining amount
if not node.left and not node.right:
if remaining == 0:
# tc: O(h) to copy path, since path keeps mutating
res.append(path[:])
# Path 2:
# a node with only one child is NOT a leaf, keep exploring
# whichever sides exist
else:
# Explore:
# recurse into left and right subtrees with updated remaining
dfs(node.left, remaining)
dfs(node.right, remaining)
# Backtrack:
# remove this node before returning to the parent call,
# so it doesn't leak into sibling paths
path.pop()
# Start backtracking at "root"
dfs(root, targetSum)
# overall: tc O(n^2) worst case, n calls each doing O(n) path copy on a skewed tree
# overall: sc O(n) worst case, recursion stack + path, skewed tree
return resSolution 2: [Backtracking] [BFS] Iterative BFS Pre Order Per Node Path Copy Queue - Tree/BFS Pre Order Across Level No Explicit Level Sized Grouping Full Across Top Down
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
# Backtracking (Iterative):
# Same "remaining sum" and "true leaf" rules as Solution 1, but
# explored level by level via a queue instead of recursion. Since
# BFS has no call stack to backtrack on, each queue entry must carry
# its OWN independent copy of the path so far -- there's no shared
# mutable list to undo after processing a node.
# Path 1:
# empty tree has no path at all
if not root:
return []
# sc: O(n) worst case (skewed tree), holds all collected paths
res = []
# sc: O(n), holds (node, remaining, path) tuples currently queued,
# each path copy up to O(h) long
queue = deque([(root, targetSum - root.val, [root.val])])
while queue:
# Pop next node, its remaining amount, and its path so far
node, remaining, path = queue.popleft()
# New Valid path:
# true leaf -- no left AND no right child -- record this path
# only if it exactly accounts for the remaining amount
if not node.left and not node.right:
if remaining == 0:
res.append(path)
continue
# Choose + Explore:
# enqueue children that exist, each with its own path copy
# extended by that child's value -- a missing side simply
# never gets enqueued
# tc: O(h) per enqueue to copy and extend path
if node.left:
queue.append((node.left, remaining - node.left.val, path + [node.left.val]))
if node.right:
queue.append((node.right, remaining - node.right.val, path + [node.right.val]))
# overall: tc O(n^2) worst case, each of n nodes does an O(n) path copy on a skewed tree
# overall: sc O(n^2) worst case, every queued path independently copies up to O(n) values
return res78. Subsets ::3:: - Medium
Topics: Array, Backtracking, Depth First Search, Bit Manipulation
Intro
Given an integer array nums of unique elements, return all possible subsets (the power set). A subset of an array is a selection of elements (possibly none) of the array. The solution set must not contain duplicate subsets. Return the solution in any order.
| Example Input | Output |
|---|---|
| nums = [1,2,3] | [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] |
| nums = [0] | [[],[0]] |
Constraints:
1 ≤ nums.length ≤ 10
-10 ≤ nums[i] ≤ 10
All the numbers of nums are unique.
Abstraction
Given a list of numbers, all unique, return the power set.
Power Set - every possible subset, including empty and full.
Backtracking Choice: 2 Choices - include or exclude number
Set: does not matter, [0, 1] and [1, 0] are the same set.
Pseudocode
Sol 1: DFS on Global Path to Generate All Subsets
1. res = []
2. path = []
3. dfs(start):
a. res.append(path[:])
b. for i in range(start, len(nums)):
path.append(nums[i])
dfs(i+1)
path.pop()
4. dfs(0)
5. return res
Sol 2: Yield Generator For O(n)
1. path = []
2. dfs(start):
a. yield path[:]
b. for i in range(start, len(nums)):
path.append(nums[i])
yield from dfs(i+1)
path.pop()
3. generator = dfs(0)
4. res = list(generator)
5. return resSolution 1: [Backtracking] [DFS] 2 Choices Include Integer Path Or Exclude And Continue Exploration - Backtracking/Generate All Combinations Or Subsets
def subsets(self, nums: List[int]) -> List[List[int]]:
# Backtracking:
# DFS recursion builds the power set solution incrementally,
# by exploring all possible choices at each step.
# Choices:
# At each step we can either include or exclude a number,
# giving us 2 options, so we get 2^n
# Shallow Copy:
# A shallow copy means that the top level object is copied,
# but the elements inside are not duplicated,
# and still reference the original object.
# In python this is fine because integers are immutable
# tc: copy in O(n), since path can have up to n elements
# sc: copy in O(n), list of n elements added to the result list
# Backtracking Recursion Stack Storage:
# All subsets of n elements for 2^n subsets
# sc: O(n * 2^n)
res = []
# Path Storage:
# holds current subset path during recursion exploration, max length n
# sc: O(n)
path = []
def dfs(start: int):
# 2 Paths determined by 2 choices:
# include or exclude the current number
# Path 1:
# Include current number,
# stop including any further integers from here on
# tc: O(n)
res.append(path[:])
# Path 2:
# Do not include current number.
# DFS exploration will test the following next 2 choices
# tc: O(n)
for i in range(start, len(nums)):
# Prepare To Explore:
# (Will Become Path 1 on exploration)
# Add integer to exploration path
# and pass start index to ensure we only include new integers
# tc: O(1)
path.append(nums[i])
# Recurse And Explore
# tc: O(2^n)
dfs(i+1)
# Backtrack:
# Revert path 2 integer to try another integer via the for loop
# tc: O(1)
path.pop()
# Start backtracking at "root"
# tc: O(2^n)
dfs(0)
# overall: tc O(n * 2^n)
# overall: sc O(n * 2^n)
return resSolution 2: [Backtracking] [DFS] Yield Generator For Streaming To Avoid O(n * 2^n) Storage [SC Opt] - Backtracking/Generate All Combinations Or Subsets
def subsets(self, nums: List[int]) -> Iterable[List[int]]:
# Generator / Yield vs Result List:
# Used to avoid storing/holding all subsets in memory at once
# Yield:
# Using 'yield', we generate subsets one at a time instead of storing them all.
# The generator pauses at each 'yield' and resumes when the next subset is requested.
# As a result, only the current path and recursion stack are kept in memory,
# reducing auxiliary space to O(n), which is due to the size O(n) of any subset
# but still independent by the total number of subsets.
# Generator / Yield Auxiliary space (not counting the output):
# Only the current subset path and recursion stack are in memory,
# O(n) instead of O(n * 2^n)
# Path Storage:
# holds current subset path during recursion exploration, max length n
# sc: O(n)
path = []
def dfs(start):
# 2 Paths determined by 2 choices:
# include or exclude the current number
# Process Root:
# Yield the current subset (shallow copy of path)
# tc: O(n) to copy path
# sc: O(n) for copy, added to output by caller if collected
yield path[:]
# Explore All Choices:
# Iterate over remaining elements starting from index `start`
# tc: O(n) per level, total recursive calls = 2^n
for i in range(start, len(nums)):
# Build New Path:
# Append current number to path
# tc: O(1), sc: O(1)
path.append(nums[i])
yield from dfs(i + 1)
# Backtrack To Current Path:
# Remove last number to explore alternative paths
# tc: O(1), sc: O(1)
path.pop()
# Generator:
# We create a generator which is passed back to the caller.
# At this moment we have not found and stored any of the subsets
# tc: O(1) to create generator
# sc: O(1) auxiliary for the generator object itself
generator = dfs(0)
# Consuming:
# To get subsets, we need to consume the generator,
# either by Streaming or Storing In Memory
# 1. Streaming: iterate over res directly
# for subset in res:
# print(subset) -> subsets will be generated one by one
# 2. Storing In Memory: convert res to a list to store all subsets in memory:
# allSubsets = list(res) -> now all subsets are generated and stored
# Note:
# Choice 2 ignores the issue we were trying to solve of storing everything in memory at one time,
# Choice 1 would be better as at any moment we are only storing/printing O(1) subset
# Here we are forced to use Choice 2 because LeetCode's auto-grader inspects
# our function to return a value of type List[List[int]]
res = list(generator)
# overall: tc O(n * 2^n) # must generate all subsets, each copied
# overall: sc O(n) # only care about size of single subset, allowed by generator
# overall: sc O(n * 2^n) # saving size of single subset per each subset, after consuming generator
return res90. Subsets II ::1:: - Medium
Topics: Array, Backtracking, Depth First Search, Bit Manipulation
Intro
Given an integer array nums that may contain duplicates, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.
| Example Input | Output |
|---|---|
| nums = [1,2,2] | [[],[1],[1,2],[1,2,2],[2],[2,2]] |
| nums = [0] | [[],[0]] |
Constraints:
1 ≤ nums.length ≤ 10
-10 ≤ nums[i] ≤ 10
Abstraction
Given a list of numbers, with duplicates, return the power set.
Power Set - every possible subset, including empty and full.
Backtracking Choice: 2 Choices - include or exclude number
Set: order does not matter, so [0, 1] and [1, 0] are the same thing.
With duplicates: Given the list [1, 2i, 2j, 3, 4], [1, 2i, 2j] and [1, 2j, 2i] are duplicate sets.
We ignore duplicate choice forks at the same depth level, The same depth level meaning [1, 2i, ...] and [1, 2j, ...] will result in the same exploration.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] 2 Choices Include Integer Path Or Exclude And Continue Exploration While Checking Previous Index For Duplicate - Backtracking/Generate While Constraint Satisfaction
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
# Backtracking:
# Recursive function will build subsets incrementally,
# by using DFS to explore all possible choices at each step.
# Duplicate Pruning:
# Sorting brings equal values next to each other, so within a single
# level of the recursion we only take the first occurrence of a
# duplicate value and skip the rest -- this avoids generating the same
# subset more than once (the same value is still reachable deeper in
# the recursion, just not as a second choice at the same level).
# sc: O(n * 2^n) 2^n subsets each length up to n
res = []
# sc: O(n), holds current subset during recursion of max length n
path = []
# Sort ascending to bring duplicates together
nums.sort()
def dfs(start):
# New Valid subset:
# add to final res
# Path 1:
# record current subset, valid at every node not just leaves
# tc: O(n) subset max length n
res.append(path[:])
# Path 2:
# try including any element we have not added yet,
# ignoring those we have included via start index
# tc: O(n)
for i in range(start, len(nums)):
# Duplicate Pruning:
# only allow the first copy in a group of equal values at this level,
# i == start is always allowed since there's nothing valid to compare yet
if i > start and nums[i] == nums[i - 1]:
continue
# Add element to testing subset
path.append(nums[i])
# Explore:
# explore this subset and move to next index to avoid reusing current element
# tc: O(2^n) total recursive calls (all possible subsets)
dfs(i + 1)
# Backtrack:
# Revert adding element to try another element via the for loop
path.pop()
# Start backtracking at "root"
dfs(0)
# overall: tc O(n * 2^n)
# overall: sc O(n * 2^n)
return res1863. Sum of All Subset XOR Totals ::1:: - Easy
Topics: Array, Math, Backtracking, Depth First Search, Bit Manipulation, Combinatorics, Enumeration
Intro
The XOR total of an array is defined as the bitwise XOR of all its elements, or 0 if the array is empty. For example, the XOR total of the array [2,5,6] is 2 XOR 5 XOR 6 = 1. Given an array nums, return the sum of all XOR totals for every subset of nums. Note: Subsets with the same elements should be counted multiple times. An array a is a subset of an array b if a can be obtained from b by deleting some (possibly zero) elements of b.
| Example Input | Output |
|---|---|
| nums = [1,3] | 6 |
| nums = [5,1,6] | 28 |
| nums = [3,4,5,6,7,8] | 480 |
Constraints:
1 ≤ nums.length ≤ 12
1 ≤ nums[i] ≤ 20
Abstraction
Given a list of numbers, all unique, return the power set.
Power Set - every possible subset, including empty and full.
Backtracking Choice: 2 Choices - include or exclude number
Set: does not matter, [0, 1] and [1, 0] are the same.
As we generate the power set, we keep a running XOR.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] 2 Choices Include Integer Path Or Exclude And Continue Exploration With Final XOR - Backtracking/Generate All Combinations Or Subsets
def subsetXORSum(self, nums: List[int]) -> int:
# Backtracking:
# Recursive function will explore every subset incrementally,
# by using DFS to explore all possible choices at each step,
# the same traversal shape as generating all subsets (LC 78),
# except instead of storing each subset we accumulate its XOR total.
# At each step we can either include or exclude a number,
# giving us 2 options, so we get 2^n subsets total.
# sc: O(n), holds current path during recursion of max length n
path = []
# sc: O(1), running sum of every subset's XOR total
self.total = 0
# tc: O(n * 2^n) 2^n subsets, each XOR computed and summed
# sc: O(n) recursion stack + path, no result list needed
def dfs(start: int, curXor: int):
# New Valid subset:
# add its XOR total to the running sum
# Path 1:
# stop including any further integers from here on, so no need to keep exploring
# tc: O(1)
self.total += curXor
# Path 2:
# try including any integer we have not added yet,
# ignoring those we have included via start index
# tc: O(n)
for i in range(start, len(nums)):
# Add integer to testing subset
path.append(nums[i])
# Explore:
# explore this subset, XOR the new number into curXor,
# and pass start index for including new integers
# tc: O(2^n)
dfs(i + 1, curXor ^ nums[i])
# Backtrack:
# Revert adding integer to try another integer via the for loop
path.pop()
# Start backtracking at "root" with an empty subset (XOR total 0)
dfs(0, 0)
# overall: tc O(n * 2^n)
# overall: sc O(n)
return self.totalSolution 2: [Bit Manipulation] OR Then Shift Bit Contribution Counting - Bit Manipulation/Counting Set Bit Contributions Across All Subsets
def subsetXORSum(self, nums: List[int]) -> int:
# Bit Manipulation:
# Instead of enumerating all 2^n subsets, we reason about each bit
# position independently: for a fixed bit position, how many of the
# 2^n subsets have that bit set in their XOR total?
# Key Insight:
# A given bit position ends up set in a subset's XOR total only if an
# ODD number of elements in that subset have that bit set. If at least
# one number in nums has a bit set, then across all 2^n subsets,
# exactly half (2^(n-1)) of them contain an odd count of that bit --
# this holds because for every subset with an odd count, pairing it
# with/without any one fixed "bit-set" element flips it to an even
# count and back, giving a perfect 1:1 split between odd and even.
# If NO number has that bit set, 0 subsets contribute it, obviously.
# Reduction:
# So a bit contributes to the final sum only if it's set in at least
# one number (captured by OR-ing every number together), and when it
# does contribute, it contributes exactly 2^(n-1) times. This means
# the final answer is just: (OR of all nums) * 2^(n-1), which is the
# same as left-shifting the OR result by (n - 1) bits.
# sc: O(1), single accumulator
orTotal = 0
# tc: O(n), OR every number together
for num in nums:
# Combine:
# OR in this number's bits, any bit set here is set in at least
# one subset element and will contribute to the final sum
orTotal |= num
# overall: tc O(n)
# overall: sc O(1)
return orTotal << (len(nums) - 1)39. Combination Sum ::4:: - Medium
Topics: Array, Backtracking, Depth First Search
Intro
Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order. The same number may be chosen from candidates an unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different. The test cases are generated such that the number of unique combinations that sum up to target is less than 150 combinations for the given input.
| Example Input | Output |
|---|---|
| candidates = [2,3,6,7], target = 7 | [[2,2,3],[7]] |
| candidates = [2,3,5], target = 8 | [[2,2,2,2],[2,3,3],[3,5]] |
| candidates = [2], target = 1 | [] |
Constraints:
1 ≤ candidates.length ≤ 30
2 ≤ candidates[i] ≤ 40
All the numbers of nums are unique.
1 ≤ target ≤ 40
Abstraction
Given a list of integers, return all possible combinations that add up to target.
All combinations: assortment of elements using elements multiple or no times where order does not matter
Backtracking Choice: 2 Choices at each step - include or exclude the current number, but including it does NOT advance the index (reuse allowed), and stop adding copies of the current number once sum > target
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Early Pruning Sorted Forward Backtracking On Global Path - Backtracking/Generate While Constraint Satisfaction
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
# Backtracking:
# Recursive function will build valid combinations incrementally,
# by using DFS to explore only choices that can still reach the target.
# Early Pruning:
# Sorting candidates ascending lets us skip candidates that exceed the
# remaining sum before ever recursing into them, pruning branches from
# the root itself instead of recursing first and discovering they're invalid.
# sc: O(n)
res = []
# sc: O(depth), holds current subset during recursion
path = []
# Sort ascending to allow early pruning
candidates.sort()
def dfs(start, remaining):
# New Valid combination:
# add to final res
# Path 1:
# remaining sum has hit zero, no further candidates needed
# tc: O(depth) subset max length depth
if remaining == 0:
res.append(path[:])
return
# Path 2:
# try including any candidate we have not added yet,
# ignoring those we have skipped via start index
# tc: O(n)
for i in range(start, len(candidates)):
candidate = candidates[i]
# Early Pruning:
# candidates are sorted ascending,
# so if this one exceeds remaining all others after it will too,
# so we can stop exploring this branch entirely
if candidate > remaining:
break
# Add candidate to testing subset
path.append(candidate)
# Explore:
# explore this subset and pass same index to allow reuse of current candidate
# tc: O(n^(target/min_cand))
dfs(i, remaining - candidate)
# Backtrack:
# Revert adding candidate to try another candidate via the for loop
path.pop()
# Start backtracking at "root"
dfs(0, target)
# overall: tc O(n^(target/min_cand))
# overall: sc O(target/min_cand + output size)
return res40. Combination Sum II ::2:: - Medium
Topics: Array, Backtracking, Depth First Search
Intro
Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target. Each number in candidates may only be used once in the combination. Note: The solution set must not contain duplicate combinations.
| Example Input | Output |
|---|---|
| candidates = [10,1,2,7,6,1,5], target = 8 | [[1,1,6], [1,2,5], [1,7], [2,6]] |
| candidates = [2,5,2,1,2], target = 5 | [[1,2,2], [5]] |
Constraints:
1 ≤ candidates.length ≤ 100
2 ≤ candidates[i] ≤ 30
1 ≤ target ≤ 30
Abstraction
Given a list of integers, return all possible combinations that add up to target, which happens to be the same as all sets within the power set that add up to the target
All combinations: assortment of elements using elements one or no times where order does not matter All sets in power set: collection of subsets of list that add up to element
Backtracking Choice: 2 Choices - include or exclude number
Set: order does not matter, so [0, 1] and [1, 0] are the same thing.
With duplicates, ignore choice forks at the same depth level, we have already hit: Given the list: [1, 2i, 2j, 3, 4] - [1, 2i, 2j] and [1, 2j, 2i] are duplicates.
The same depth level meaning [1, 2i, ...] and [1, 2j, ...] will result in the same exploration.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: Recursive Sorted Early Pruning Forward Backtracking on Current Combination - Backtracking/Generate While Constraint Satisfaction
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
# Backtracking:
# Recursive function will build valid combinations incrementally,
# by using DFS to explore only choices that can still reach the target.
# Unlike combinationSum, each candidate can only be used once, and the
# input may contain duplicate values that must not produce duplicate combinations.
# Early Pruning:
# Sorting candidates ascending lets us skip candidates that exceed the
# remaining sum before ever recursing into them, pruning branches from
# the root itself instead of recursing first and discovering they're invalid.
# Duplicate Pruning:
# Sorting also aligns equal values next to each other, so within a single
# level of the recursion we only take the first occurrence of a duplicate
# value and skip the rest -- this avoids generating the same combination
# more than once (a later, later-occurring duplicate is still reachable
# deeper in the recursion, just not as a second choice at the same level)
# sc: O(depth)
res = []
# sc: O(depth), holds current subset during recursion
path = []
# Sort ascending to allow early pruning and duplicate pruning
candidates.sort()
def dfs(start, remaining):
# Process Root -> : check if valid combination, shallow copy
if remaining == 0:
res.append(path[:])
return
# Process Choices -> : explore numbers from current index onward
for i in range(start, len(candidates)):
# Duplicate Pruning:
# Only use the first copy of an integer, check if previous index is a duplicate.
# i > start ensures the first iteration is always valid,
# only candidates after the first iteration get compared for avoiding negative indexing
if i > start and candidates[i] == candidates[i - 1]:
continue
# Early Pruning:
# candidates are sorted ascending, so if this one exceeds remaining
# all others after it will too, so we can stop exploring this branch entirely
if candidates[i] > remaining:
break
# Add candidate to testing subset
path.append(candidates[i])
# Explore:
# explore this subset and move to next index, since each candidate
# can only be used once (no reuse of current index, unlike combinationSum)
# tc: O(2^n) in the worst case, fewer in practice due to pruning
dfs(i + 1, remaining - candidates[i])
# Backtrack:
# Revert adding candidate to try another candidate via the for loop
path.pop()
# Start backtracking at "root"
dfs(0, target)
# overall: tc O(n * 2^n)
# overall: sc O(n + output size)
return res216. Combination Sum III ::2:: - Medium
Topics: Array, Backtracking, Depth First Search
Intro
Find all valid combinations of k numbers that sum up to n such that the following conditions are true: Only numbers 1 through 9 are used. Each number is used at most once. Return a list of all possible valid combinations. The list must not contain the same combination twice, and the combinations may be returned in any order.
| Example Input | Output |
|---|---|
| k = 3, n = 7 | [[1,2,4]] |
| k = 3, n = 9 | [[1,2,6],[1,3,5],[2,3,4]] |
| k = 4, n = 1 | [] |
Constraints:
2 ≤ k ≤ 9
1 ≤ n ≤ 60
Abstraction
Given a list of integers [1, 2, 3, 4, 5, 6, 7, 8, 9], return all possible combinations that add up to target, which happens to be the same as all sets within the power set that add up to the target
All combinations: assortment of elements using elements one or no times where order does not matter All sets in power set: collection of subsets of list that add up to element
Backtracking Choice: 2 Choices - include or exclude number
Set: order does not matter, so [0, 1] and [1, 0] are the same thing.
With duplicates: Given the list [1, 2i, 2j, 3, 4], [1, 2i, 2j] and [1, 2j, 2i] are duplicate sets.
We ignore duplicate choice forks at the same depth level, The same depth level meaning [1, 2i, ...] and [1, 2j, ...] will result in the same exploration.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Early Pruning Recursive Sorted Forward Backtracking On Global Path With Count Constraint - Backtracking/Generate While Constraint Satisfaction
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
# Backtracking:
# Recursive function will build valid combinations incrementally,
# by using DFS to explore only choices that can still reach both the
# target sum and the exact count k, same shape as Combination Sum
# (LC 39), with two constraints tracked together instead of one.
# Early Pruning:
# Candidates 1-9 are naturally sorted ascending, so once a candidate
# exceeds the remaining sum, every candidate after it will too --
# prune that branch immediately instead of recursing into it.
# Count Pruning:
# Since each number is used at most once and only 1-9 are available,
# we can also stop exploring a branch early if there aren't enough
# remaining candidates left to reach k numbers, or if the path has
# already reached k numbers without reaching n.
# sc: O(k)
res = []
# sc: O(k), holds current path during recursion, max length k
path = []
def dfs(start: int, remaining: int):
# New Valid combination:
# add to final res
# Path 1:
# path has exactly k numbers and remaining sum has hit zero
# tc: O(k) subset max length k
if len(path) == k and remaining == 0:
res.append(path[:])
return
# Path 2:
# path already has k numbers but sum hasn't hit zero, or no
# candidates remain -- neither can lead to a valid combination
if len(path) == k or start > 9:
return
# Path 3:
# try including any candidate from 'start' through 9 we have not added yet
# tc: O(9) per call, bounded by fixed digit range
for candidate in range(start, 10):
# Early Pruning:
# candidates are ascending, so if this one exceeds remaining
# all others after it will too, so we can stop exploring this branch entirely
if candidate > remaining:
break
# Add candidate to testing subset
path.append(candidate)
# Explore:
# explore this subset and move to next digit, since each candidate
# can only be used once (no reuse of current digit)
# tc: O(C(9, k)) total combinations in the worst case, fewer with pruning
dfs(candidate + 1, remaining - candidate)
# Backtrack:
# Revert adding candidate to try another candidate via the for loop
path.pop()
# Start backtracking at "root"
dfs(1, n)
# overall: tc O(C(9, k) * k)
# overall: sc O(k)
return res22. Generate Parentheses ::4:: - Medium
Topics: String, Stack, Dynamic Programming, Backtracking, Depth First Search
Intro
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
| Input | Output |
|---|---|
| 1 | ["()"] |
| 3 | ["((()))","(()())","(())()","()(())","()()()"] |
Constraints:
1 ≤ n ≤ 8
Abstract
Given a number, which will determine the list of parentheses ['(', '(', ')', ')'] return all possible combinations that have a valid sequence of parentheses.
All combinations: assortment of elements using elements one or no times where order does not matter All sets in power set: collection of subsets of list that have a valid sequence of parentheses.
Backtracking Choice: 2 Choices - include or exclude parentheses
Set: order does not matter, so [0, 1] and [1, 0] are the same thing.
With duplicates, ignore choice forks at the same depth level, we have already hit: Given the list: ['(', '(', ')', ')'] - ['(', '(', ')', ')'] and ['(', ')', '(', ')'] are duplicates.
The same depth level meaning ['(', '(', ...] and ['(', '(', ...] will result in the same exploration.
To avoid that, we don't actually iterate over a list, we pretend to encounter them via 2 if statements and only add parentheses when the conditions allow.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive with Mutable List Appending - Stack/Backtracking by Tracking History or State
def generateParenthesis(self, n: int) -> List[str]:
# Backtracking:
# Recursive function will build valid parenthesis sequences incrementally,
# by using DFS to explore only choices that keep the sequence valid so far.
# List Mutability:
# We use a single list 'curr' and modify it in place using append/pop.
# The list only gets converted when we find a valid combination of length O(n)
# There are Catalan(n) valid sequences each of length O(n)
# sc: O(Catalan(n) * n), holds all valid sequences once complete
res = []
def dfs(current, openCount, closeCount):
# New Valid sequence:
# add to final res
# Path 1:
# both open and close counts have reached n, valid sequence found
# tc: O(n) to join current into a string
if openCount == n and closeCount == n:
res.append("".join(current))
return
# Path 2a:
# add an open paren, if don't have n total yet,
# ensuring the sequence never has more '(' than allowed
if openCount < n:
# Choose:
# append open '('
current.append('(')
# Explore:
# recurse with updated counts
dfs(current, openCount + 1, closeCount)
# Backtrack:
# remove last open '('
current.pop()
# Path 2b:
# add a close paren, if we have less than the number of open paren,
# ensuring every close paren has an open paren to pair with it
if closeCount < openCount:
# Choose:
# append close ')'
current.append(')')
# Explore:
# recurse with updated counts
dfs(current, openCount, closeCount + 1)
# Backtrack:
# remove last close ')'
current.pop()
# Start backtracking at "root", empty list passed
backtrack([], 0, 0)
# overall: tc O(Catalan(n) * n)
# overall: sc O(Catalan(n) * n)
return resSolution 2: [Dynamic Programming] Two Pointer Opposite Ends Catalan Pattern To Build Parentheses Combinations - Stack/Dynamic Programming State Compression
def generateParenthesis(self, n: int) -> List[str]:
# Dynamic Programming:
# We exploit the recursive structure of Catalan numbers
# by building a list of all valid parentheses combinations for each level n
# Two Pointer:
# If we have a list of valid combinations of parenthesis,
# we can append them in a format that generate more valid combinations
# dp: list of list of parentheses combinations
# dp[n]: list containing all valid parenthesis combinations for the nth level (n pair of parenthesis)
dp = []
for _ in range(n + 1):
dp.append([])
# Base case: valid combination for n = 0
dp[0] = [""]
# Iterate: create valid lists from dp[1] to dp[n]
# Current: building list for ith pair
# tc: iterate over list of n length O(n)
for i in range(1, n + 1):
# iterate over stored lists up to now
for j in range(i):
# Opposite Ends Two Pointer Variation:
# Since pointers will meet in middle and cross each other,
# they will get all variations of combinations already built,
# which allows us to format them to generate more valid combinations
# Setup: Opposite Ends Two Pointer Variation
# Forward and Reverse Iteration
# Forward Iteration List 1:
# Iterate over each valid string of parentheses for level j,
# starting at first parentheses pairs level of 0 pairs of parenthesis
for left in dp[j]:
# Reverse Iteration List 2:
# Iterate over each valid string of parentheses for level (i - 1 - j),
# starting at last parenthesis pairs level of largest number pairs currently available
for right in dp[i - 1 - j]:
# Generation Format:
# ({left}){right} or {left}({right}) are both valid formulas
# Since we are doing Opposite Ends and left and right will cross over each other,
# left and right will both individually use all values
# which means with these 2 iterations
# we are eventually doing both the same thing
# dp[i].append(f"{left}({right})")
dp[i].append(f"({left}){right}")
# overall: tc O(Catalan(n) * n)
# overall: sc O(Catalan(n) * n)
return dp[n]46. Permutations ::1:: - Medium
Topics: Array, Backtracking, Depth First Search
Intro
Given an array nums of distinct integers, return all the possible . You can return the answer in any order. A permutation is a rearrangement of all the elements of an array.
| Example Input | Output |
|---|---|
| nums = [1,2,3] | [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] |
| nums = [0,1] | [[0,1],[1,0]] |
| nums = [1] | [[1]] |
Constraints:
1 ≤ nums.length ≤ 6
2 ≤ nums[i] ≤ 30
All the integers of nums are unique.
Abstraction
Given a list of numbers, all unique so no duplicates, return all possible permutations.
All Permutations: every possible ordering of the list
Backtracking Choice: N choices at each step - pick any unused number for the current position (N choices for position 1, N-1 for position 2, etc.)
Permutation: order does matter and must use all elements: [0, 1] and [1, 0] are distinct permutations.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive DFS Backtracking on Current Permutation Path with Element Usage Tracking - Backtracking/Generate While Constraint Satisfaction
def permute(self, nums: List[int]) -> List[List[int]]:
# Backtracking:
# Recursive function will build permutations incrementally,
# by using DFS to explore every not yet used element at each position.
# Element Usage Tracking:
# Unlike subset/combination problems,
# order matters here and every element must appear exactly once per permutation
# So instead of a start index,
# track elements already placed in the current path with a 'used' boolean array,
# and skip those.
# Worst case all distinct values
# sc: O(n * n!)
res = []
# Backtracking Path:
# holds current path during recursion, max length n
# sc: O(n)
path = []
n = len(nums)
# Used Elements Tracker:
# tracks which elements are already in the current path
# sc: O(n)
used = [False] * n
def dfs():
# New Valid permutation:
# add to final res
# Reached target path length:
# found new valid permutation of elements
# tc: O(n)
if len(path) == n:
res.append(path[:])
return
# Explore Remaining Unused Numbers:
# not reached target path length yet,
# continue path generation (N!, N-1, N-2, ...)
# adding all elements currently marked as not used yet to depth d
# tc: O(n)
for i in range(n):
# No Reusing Elements:
# skip elements already in path
if used[i]:
continue
# Try new element at this depth
path.append(nums[i])
# Mark element as used
used[i] = True
# Explore:
# explore this path with new element at this depth
# to generate all possible permutations with remaining unused elements
# tc: O(n!)
dfs()
# Backtrack:
# Remove new element at this depth from path
# and mark as unused for future adding
path.pop()
used[i] = False
# Start backtracking at root with empty list and all elements unused
dfs()
# overall: tc O(n * n!)
# overall: sc O(n)
return res47. Permutations II ::1:: - Medium
Topics: Array, Backtracking, Depth First Search, Sorting
Intro
Given a collection of numbers, nums, that might contain duplicates, return all possible unique permutations in any order.
| Example Input | Output |
|---|---|
| nums = [1,1,2] | [[1,1,2], [1,2,1], [2,1,1]] |
| nums = [1,2,3] | [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] |
Constraints:
1 ≤ nums.length ≤ 8
-10 ≤ nums[i] ≤ 10
Abstraction
Given a list of numbers, duplicates allowed, return all possible permutations.
All Permutations: every possible ordering of the list
Backtracking Choice: N choices at each step - pick any unused number for the current position (N choices for position 1, N-1 for position 2, etc.)
Permutation: order does matter and must use all elements, so [0, 1] and [1, 0] are two distinct permutations.
With duplicates: Given the list [1, 2i, 2j, 3, 4], [1, 2i, 2j] and [1, 2j, 2i] are duplicate sets.
We ignore duplicate choice forks at the same depth level, The same depth level meaning [1, 2i, ...] and [1, 2j, ...] will result in the same exploration.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive DFS Backtracking On Current Permutation Path With Sorted Duplicate Skipping - Backtracking/Generate While Constraint Satisfaction
def permuteUnique(self, nums: list[int]) -> list[list[int]]:
# Backtracking:
# Recursive function will build permutations incrementally,
# by using DFS to explore every not yet used element at each position,
# and we skip duplicate values so no permutation is generated more than once.
# Element Usage Tracking:
# Order matters here and every element must appear exactly once per
# permutation, so instead of a start index we track which elements
# have already been placed in the current path with a 'used' boolean
# array, and skip those.
# Duplicate Pruning:
# Sorting brings equal values next to each other.
# orting allows us to block multile occurences of the same integer,
# from being used at the same index in the permutation
# [1a, 1b, 2, 3] => [1b, 1a, 2, 3] => [1, 1, 2, 3]
# 0 1 2 3 0 1 2 3
# by avoiding both 1's being put at index 0, which lead to duplicate sets
# [2, 1a, 1b, 3] => [2, 1b, 1a, 3] => [2, 1, 1, 3]
# 0 1 2 3 0 1 2 3
# by avoiding both 1's being put at index 0, which lead to duplicate sets
# etc...
# Best case all n elements are identical, list of n size O(n)
# Worst case all distinct values O(n * n!)
# sc: O(n * n!)
res = []
# holds current path during recursion, max length n
# sc: O(n)
path = []
n = len(nums)
# tracks which elements are already in the current path
# sc: O(n)
used = [False] * n
# Sort ascending so duplicates sit next to each other
nums.sort()
def dfs():
# New Valid permutation:
# add to final res
# Path 1:
# path length has reached n, every element has been placed
# tc: O(n) subset max length n
if len(path) == n:
res.append(path[:])
return
# Path 2:
# continue path generation (N!, N-1, N-2, ...) adding any element not already in path
# tc: O(n)
for i in range(n):
# No Reusing Elements:
# skip elements already in path
if used[i]:
continue
# Duplicate Pruning:
# skip a duplicate value for the current index to avoid duplicates at the same index
# [1a, 1b, 2, 3] => [1b, 1a, 2, 3]
# 0 1 2 3 0 1 2 3
# when used[i-1] == True,
# that means we can place the duplicate without risk of duplicate combinations,
# since that means a previous duplicate has already been placed,
# we do not have to worry about the current duplicate being placed
# at the same index as the previous
# but when used[i-1] == False,
# that means we are at risk of duplicating a previous combination,
# since that means a previous duplicate has been placed then removed,
# so there is a chance the current duplicate is going to be placed
# at the same previous depth
if 0 < i and nums[i] == nums[i - 1] and not used[i - 1]:
continue
# Add new element
path.append(nums[i])
used[i] = True
# Explore:
# explore this path and to reach all possible permutations,
# will add remaining unused elements
# tc: O(n!)
dfs()
# Backtrack:
# Remove element from path and allow future adding
path.pop()
used[i] = False
# Start backtracking at root with empty list and all elements unused
dfs()
# overall: tc O(n * n!)
# overall: sc O(n)
return res79. Word Search ::2:: - Medium
Topics: Array, String, Backtracking, Depth First Search, Matrix
Intro
Given an m x n grid of characters board and a string word, return true if word exists in the grid. The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.
| Example Input | Output |
|---|---|
| board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" | true |
| board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE" | true |
Constraints:
m == board.length
n = board[i].length
1 ≤ m, n ≤ 6
1 ≤ word.length ≤ 15
board and word consists of only lowercase and uppercase English letters.
Follow up: Could you use search pruning to make your solution faster with a larger board?
Abstraction
Find if word exists in grid.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] DFS Backtracking on Current Path - Backtracking/Path Finding in Search Space
def exist(self, board: list[list[str]], word: str) -> bool:
# Backtracking:
# Recursive function will trace a path through the grid incrementally,
# by using DFS to try extending the current path in all 3 directions,
# only continuing a branch while it still matches the word so far.
# Board len
rows, cols = len(board), len(board[0])
# tc: O(L) per call chain ~ O(L^3)
def dfs(r, c, wordIndex):
# Target Found:
# every character in word has been matched
if wordIndex == len(word):
return True
# Out Of Bounds:
# cell is outside the grid, can't explore in this direction
if r < 0 or c < 0 or rows <= r or cols <= c:
return False
# Letter Isn't The Expected Letter:
# only continue exploration if we can keep forming the word
if board[r][c] != word[wordIndex]:
return False
# Correct Letter and In Bounds, Mark Cell As Overwritten:
# mark current cell as visited by temporarily overwriting it,
# so the same cell can't be reused later in this same path
tmp = board[r][c]
board[r][c] = "#"
# Explore:
# recurse in all 3 directions, short circuit on first match found
# we actually at every cell is explore 4 ways, but 1 fails immediately since
# we can't go back to the previous cell we just came since we marked it with '#'
# tc: O(3^L)
found = (dfs(r+1, c, wordIndex+1) or
dfs(r-1, c, wordIndex+1) or
dfs(r, c+1, wordIndex+1) or
dfs(r, c-1, wordIndex+1))
# Backtrack:
# revert cell from '#' to original value so other paths can use it
board[r][c] = tmp
return found
# Starting DFS from every cell on the board
# tc: O(r * c)
firstLetter = word[0]
for r in range(rows):
for c in range(cols):
if board[r][c] == firstLetter:
if dfs(r, c, 0):
return True
# overall: tc O(rows * cols * 3^L)
# overall: sc O(L), recursion stack depth bounded by word length
return FalseSolution 2: [Follow Up] [Backtracking] [DFS] Word Reversal Optimization DFS Backtracking on Current Path - Backtracking/Path Finding in Search Space
def exist(self, board: list[list[str]], word: str) -> bool:
# Larger Board Optimizations
# Backtracking:
# - Recursive function will trace a path through the grid incrementally,
# by using DFS to try extending the current path in all 3 directions,
# only continuing a branch while it still matches the word so far.
# Work Minimum Count Early Exit:
# - If the board doesn't contain enough of any letter the word
# needs, no path can possibly exist -- return False immediately.
# Rarer First Or Last Letter Search Start DFS Branch Optimization:
# - Starting the search from either the first or last letter,
# depending on which is rarer (less occurrences),
# will reduces how many cells trigger a full DFS attempt,
# (less cells will trigger a DFS attempt, since there are less
# of the expected starting letter),
# and tend to fail faster when theres no valid path
# as there are less DFS so less branches explored overall
# Board len
rows, cols = len(board), len(board[0])
# Fixed alphabet size (26 letters) regardless of input size
# tc: O(L)
# sc: O(26) ~ O(1)
wordCount = defaultdict(int)
for c in word:
wordCount[c] += 1
# tc: O(r * c)
# sc: O(r * c)
boardCount = defaultdict(int)
for r in range(rows):
for c in range(cols):
boardCount[board[r][c]] += 1
# Early Word Pruning:
# if the board doesn't contain enough of any needed letter,
# no path can possibly spell out the word
# tc: O(len(word)) to check every distinct letter needed
for char, count in wordCount.items():
if boardCount[char] < count:
return False
# Start FEarly Recurse Optimization:
# search from whichever end of the word is rarer on the board,
# this will trigger less DFS search attempts
firstLetterCount = boardCount[word[0]]
lastLetterCount = boardCount[word[-1]]
if lastLetterCount < firstLetterCount:
word = word[::-1]
# tc: O(L) per call chain ~ O(L^3)
def dfs(r, c, wordIndex):
# Target Found:
# every character in word has been matched
if wordIndex == len(word):
return True
# Out Of Bounds:
# cell is outside the grid, can't continue in this direction
if r < 0 or c < 0 or r >= rows or c >= cols:
return False
# Letter Isn't The Expected Letter:
# only continue exploration if we can keep forming the word
if board[r][c] != word[wordIndex]:
return False
# Correct Letter and In Bounds, Mark Cell As Overwritten:
# mark current cell as visited by overwriting it, so the same
# cell can't be reused later in this same path
tmp = board[r][c]
board[r][c] = "#"
# Explore:
# recurse in all 3 directions, short circuit on first match found
# we actually at every cell is explore 4 ways, but 1 fails immediately since
# we can't go back to the previous cell we just came since we marked it with '#'
# tc: O(3^L)
found = (dfs(r+1, c, wordIndex+1) or
dfs(r-1, c, wordIndex+1) or
dfs(r, c+1, wordIndex+1) or
dfs(r, c-1, wordIndex+1))
# Backtrack:
# revert cell from '#' to original value so other paths can use it
board[r][c] = tmp
return found
# Starting DFS from every cell on the board
# tc: O(r * c)
firstLetter = word[0]
for r in range(rows):
for c in range(cols):
if board[r][c] == firstLetter:
if dfs(r, c, 0):
return True
# overall: tc O(r * c * 3^L), L = len(word)
# overall: sc O(L), recursion stack depth bounded by word length
return False212. Word Search II ::2:: - Hard
Topics: Array, String, Backtracking, Depth First Search, Trie, Matrix
Intro
Given an m x n board of characters and a list of strings words, return all words on the board. Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
| Example Input | Output |
|---|---|
| look at LeetCode question diagram | ["eat","oath"] |
Constraints:
m == board.length
n == board[i].length
1 ≤ m, n ≤ 12
board[i][j] is a lowercase English letter.
1 ≤ words.length ≤ 3 * 104
1 ≤ works[i].length ≤ 10
words[i] consists of lowercase English letters.
All the strings of words are unique.
Abstraction
Given a board and a list of words, return all words from the list that are present on the board.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] [Trie] Trie Implementation Optimal - Trie/Trie Insert and Search Recursive
# A TrieNode Has:
# - dict from (c -> TreeNode)
# - bool saying it this node is the end of a word
class TrieNode:
# Node within Trie Structure
def __init__(self):
# Dict of all words found tree,
# L of words * number of words
# sc: O(L * W)
self.subtrees = {}
# Marks whether this node is the end of a valid word
self.word = None
# Trie Structure Add New Word:
# - add word to prefix structure currently held by Trie
def addWord(self, word):
# addWord() is called like this:
# - root = TrieNode()
# - root.addWord(word)
# Self is passed as a TrieNode,
# create an iterator so we can traverse across the Trie Structure
currNode = self
# Add char from word
# tc: O(L)
for c in word:
# Add TrieNode Path:
# current node does not point to char we are adding,
# creating new node
if c not in currNode.subtrees:
currNode.subtrees[c] = TrieNode()
# Jump to next TrieNode in word path
currNode = currNode.subtrees[c]
# End Flag:
# reached end of node flag for end of word
currNode.word = word
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
# Backtracking + Trie:
# Same core idea as Solution 1 -- DFS from every cell, only continuing
# a path while it matches a valid prefix in the Trie built from 'words'.
# Optimization vs Solution 1:
# 1. In-place marking: instead of a separate 'visit' set, we
# temporarily overwrite the board cell itself ('#'), avoiding the
# extra O(L) set and its hashing overhead per cell.
# 2. Leaf pruning: once a Trie subtree has no children left (every
# word through it has already been found), we delete it from its
# parent. This shrinks the Trie as words are found, so later DFS
# calls stop earlier instead of re-exploring dead branches.
# Trie Structure:
#
# ROOT
# / \
# c d
# | |
# a o
# / \ |
# t r g
# Root TrieNode holds all starting valid chars for all words,
# and the Trie structure contains the words themselves
# sc: O(L * W)
root = TrieNode()
for word in words:
root.addWord(word)
# Board len
rows, cols = len(board), len(board[0])
# sc: O(L * W)
res = []
def dfs(r, c, parent):
# Out Of Bounds:
# cell is outside the grid, can't continue in this direction
if r < 0 or c < 0 or rows <= r or cols <= c:
return
# Grab current char
letter = board[r][c]
# Letter Isn't One Of The Expected Letters, Or '#':
# only continue exploration if we can keep forming the word
if letter not in parent.subtrees:
return
# Jump to the TrieNode representing the current character
currNode = parent.subtrees[letter]
# New Valid word:
# this node marks the end of a word in the Trie,
# add word to the result list,
# remove stored word to prevent duplicates of the same word being added
if currNode.word:
res.append(currNode.word)
currNode.word = None
# Jump to next Trie node
currNode = parent.subtrees[letter]
# Choose:
# mark current cell as visited in place, so it can't be reused
# later in this same path
board[r][c] = "#"
# Explore:
# recurse in all 4 directions, only into cells that are both
# in-bounds and still a valid next character in the Trie
# tc: O(3^L)
dfs(r+1, c, currNode)
dfs(r-1, c, currNode)
dfs(r, c+1, currNode)
dfs(r, c-1, currNode)
# Backtrack:
# revert cell from '#' to original value
board[r][c] = letter
# Dead Last Char Pruning:
# This TrieNode is not the end of a word.
# If it also has no remaining subtrees, we are safe to remove it
# to prevent unnecessary future DFS exploration
if not currNode.subtrees:
parent.subtrees.pop(letter)
# Starting DFS from every cell on the board
# tc: O(r * c)
for r in range(rows):
for c in range(cols):
if board[r][c] in root.subtrees:
dfs(r, c, root)
# overall: tc O(r * c * 3^L)
# overall: sc O(W)
return res131. Palindrome Partitioning ::2:: - Medium
Topics: String, Dynamic Programming, Backtracking, Depth First Search
Intro
Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitioning of s.
| Example Input | Output |
|---|---|
| s = "aab" | [["a","a","b"],["aa","b"]] |
| s = "a" | [["a"]] |
Constraints:
1 ≤ s.length ≤ 16
s contains only lowercase English letters.
Abstraction
Split string s in a way such that every substring of the partition is a palindrome.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] DFS Backtracking on Current Path to Match Word - Backtracking/Path Finding in Search Space
def partition(self, s: str) -> List[List[str]]:
# Backtracking:
# Recursive function will build partitions incrementally, by using
# DFS to try every possible next substring and only continuing a
# branch while that substring is a palindrome.
# sc: O(n) worst case, holds all valid partitions
res = []
# sc: O(n), holds current partition during recursion, max length n
path = []
def isPalindrome(l, r) -> bool:
# Check candidate substring s[l:r+1] from both ends inward
# tc: O(n) per call in the worst case
while l < r:
if s[l] != s[r]:
return False
l += 1
r -= 1
return True
def dfs(start):
# New Valid partition:
# reached end of string, every character has been partitioned
if start == len(s):
res.append(path[:])
return
# Path 1:
# try every substring starting at 'start', extending one char at a time
for end in range(start, len(s)):
# Early Pruning:
# only continue this branch if the candidate substring is a palindrome
# tc: O(n) per check
if isPalindrome(start, end):
# Choose:
# add palindromic substring to path
path.append(s[start:end + 1])
# Explore:
# recurse starting right after this substring
dfs(end + 1)
# Backtrack:
# remove last substring to try the next end position
path.pop()
# Start backtracking at "root"
dfs(0)
# overall: tc O(n * 2^n), 2^n possible partitions worst case, each palindrome check O(n)
# overall: sc O(n), recursion stack + path (auxiliary, excluding output)
return resSolution 2: [Backtracking] [DFS] DFS Backtracking on Current Path to Match Word - Backtracking/Path Finding in Search Space
def partition(self, s: str) -> List[List[str]]:
# Backtracking:
# Same traversal shape as Solution 1 -- build partitions incrementally
# via DFS, only continuing a branch while the candidate substring is
# a palindrome.
# Optimization vs Solution 1:
# Solution 1 re-checks whether a substring is a palindrome from
# scratch every time it's considered, O(n) per check, even though the
# same substrings get re-examined across different branches. Here we
# precompute every substring's palindrome status once in O(n^2) using
# DP, so each check during backtracking becomes an O(1) lookup.
n = len(s)
# sc: O(n) worst case, holds all valid partitions
res = []
# sc: O(n), holds current partition during recursion, max length n
path = []
# sc: O(n^2), isPal[i][j] is True if s[i:j+1] is a palindrome
isPal = [[False] * n for _ in range(n)]
# Backwards -> Forwards:
# to compute isPal[0][4], we need isPal[1][3] already known.
# Looping i backwards ensures that by the time we reach row i,
# row i + 1 (all substrings starting one index later) is already filled in.
# tc: O(n^2), every substring checked exactly once
for i in range(n - 1, -1, -1):
for j in range(i, n):
# Substring Length:
# (j - i + 1) <= 3 covers base cases directly: "a", "aa", "aba"
# Inner Substring:
# if s[i+1:j] is a palindrome and the outer characters match,
# then s[i:j+1] is also a palindrome
if s[i] == s[j] and ((j - i + 1) <= 3 or isPal[i + 1][j - 1]):
isPal[i][j] = True
def dfs(start):
# New Valid partition:
# reached end of string, every character has been partitioned
if start == n:
res.append(path[:])
return
# Path 1:
# try every substring starting at 'start', extending one char at a time
for end in range(start, n):
# Early Pruning:
# O(1) table lookup instead of Solution 1's O(n) re-scan
if isPal[start][end]:
# Choose:
# add palindromic substring to path
path.append(s[start:end + 1])
# Explore:
# recurse starting right after this substring
dfs(end + 1)
# Backtrack:
# remove last substring to try the next end position
path.pop()
# Start backtracking at "root"
dfs(0)
# overall: tc O(n^2 + n * 2^n), O(n^2) to build the table, O(n * 2^n) worst case for backtracking
# overall: sc O(n^2), dominated by the palindrome table (recursion stack + path are O(n))
return res51. N Queens ::2:: - Hard
Topics: Array, Backtracking, Depth First Search
Intro
The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Given an integer n, return all distinct solutions to the n queens puzzle. You may return the answer in any order. Each solution contains a distinct board configuration of the n queens' placement, where 'Q' and '.' both indicate a queen and an empty space, respectively.
| Example Input | Output |
|---|---|
| n = 4 | [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] |
| n = 1 | [["Q"]] |
Constraints:
1 ≤ n ≤ 9
Abstraction
Find all the possible ways to possible n queens on a board of n x n size.
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive DFS Backtracking on Current Row with Column/Diagonal Tracking - Backtracking/Combinatorial Optimization
def solveNQueens(self, n: int) -> List[List[str]]:
# Backtracking:
# Recursive function will place one queen per row incrementally,
# by using DFS to try every column in the current row and only
# continuing a branch while that placement doesn't threaten any
# queen already placed in an earlier row.
# sc: O(n) worst case, holds all valid board configurations
res = []
# sc: O(n^2), n x n board
board = [["."] * n for _ in range(n)]
# 3 Sets to track threatened positions:
# sc: O(n), columns currently occupied
cols = set()
# rows are inherently tracked by recursion depth, no separate set needed
# sc: O(n), r - c diagonals: all cells on this diagonal share the same row - col value
diag1 = set()
# sc: O(n), r + c diagonals: all cells on this diagonal share the same row + col value
diag2 = set()
def dfs(row: int):
# New Valid board:
# every row has a queen placed, record this board configuration
# tc: O(n^2) to join board into strings
if row == n:
res.append(["".join(r) for r in board])
return
# Path 1:
# try every column in the current row
for col in range(n):
# Early Pruning:
# skip this column if it or either diagonal is already threatened
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
# Choose:
# place queen at (row, col), mark column and both diagonals occupied
board[row][col] = "Q"
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
# Explore:
# recurse to the next row, row + 1 guarantees rows never overlap
dfs(row + 1)
# Backtrack:
# remove queen and free column/diagonals to try the next column
board[row][col] = "."
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
# Start backtracking at "root", row 0
dfs(0)
# overall: tc O(n!), n choices in row 0, at most n-1 in row 1, pruned further by diagonals
# overall: sc O(n^2), board dominates; cols/diag1/diag2/recursion stack are each O(n)
return resSolution 2: [Backtracking] [DFS] Recursive DFS Backtracking Using Bitmasking for Columns/Diagonals - Backtracking/Combinatorial Optimization
def solveNQueens(self, n: int) -> List[List[str]]:
# Backtracking:
# Same traversal shape as Solution 1 -- place one queen per row via
# DFS, only continuing while the placement doesn't threaten an
# earlier queen -- but threatened positions are tracked with integer
# bitmasks instead of three separate sets.
# Optimization vs Solution 1:
# Solution 1's set lookups (col in cols, etc.) and the n x n board
# array add overhead per check and per placement. Here, 'cols',
# 'diag1', and 'diag2' are single integers where each bit represents
# a threatened column/diagonal position. Combining them with bitwise
# OR and checking availability with a single AND/NOT turns each row's
# "which columns are free" computation into O(1) bitwise ops instead
# of n individual set lookups, and shifting diag1/diag2 by 1 each row
# naturally re-aligns them for the next row without recomputing from
# row/col arithmetic.
# sc: O(n) worst case, holds all valid board configurations
res = []
# sc: O(n), holds chosen column index per row during recursion
path = []
def dfs(row, cols, diag1, diag2):
# New Valid board:
# every row has a queen placed, convert path (row -> column) into board strings
# tc: O(n^2) to build the board
if row == n:
board = []
for r in path:
row_str = ["." for _ in range(n)]
row_str[r] = "Q"
board.append("".join(row_str))
res.append(board)
return
# Path 1:
# compute every column in this row that's not threatened, as a bitmask
# tc: O(1) bitwise ops instead of n individual set lookups
available = ((1 << n) - 1) & (~(cols | diag1 | diag2))
# try every available column, extracting the lowest set bit each time
while available:
# Choose:
# isolate the rightmost available bit and remove it from 'available'
pos = available & -available
available &= available - 1
col = (pos - 1).bit_length()
path.append(col)
# Explore:
# recurse to the next row with updated masks; diag1/diag2 shift by
# one bit to stay aligned with the diagonal's position on the next row
dfs(row + 1, cols | pos, (diag1 | pos) << 1, (diag2 | pos) >> 1)
# Backtrack:
# remove last chosen column, cols/diag1/diag2 are never mutated in
# place (new masks are passed per call), so nothing else to undo
path.pop()
# Start backtracking at "root", row 0, no columns/diagonals threatened yet
dfs(0, 0, 0, 0)
# overall: tc O(n!), same combinatorial bound as Solution 1, fewer constant-factor ops per check
# overall: sc O(n), no n x n board maintained during recursion, only path + bitmask args
return res37. Sudoku Solver ::1:: - Hard
Topics: Array, Hash Table, Backtracking, Depth First Search, Matrix
Intro
Write a program to solve a Sudoku puzzle by filling the empty cells. A sudoku solution must satisfy all of the following rules: Each of the digits 1-9 must occur exactly once in each row. Each of the digits 1-9 must occur exactly once in each column. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid. The '.' character indicates empty cells.
| Example Input | Output |
|---|---|
| board, look at LeetCode | something! |
Constraints:
board.length == 9
board[i].length == 9
board[i][j] is a digit or '.'.
It is guaranteed that the input board has only one solution.
Abstraction
Write a program to solve sudoku
Pseudocode
oh! pseudocode hasn't been written yet, try another card! :)Solution 1: [Backtracking] [DFS] Recursive Cell By Cell Backtracking With Row/Col/Box Constraint Checking - Backtracking/Generate While Constraint Satisfaction
def solveSudoku(self, board: List[List[str]]) -> None:
# Backtracking:
# Same traversal shape as Solution 1 -- fill empty cells incrementally
# via DFS, trying every digit 1-9 and only keeping placements that
# satisfy the row, column, and 3x3 box constraints.
# Optimization vs Solution 1:
# Solution 1's isValid() rescans the entire row, column, and box from
# scratch on every single check, O(n) per check. Here we instead
# maintain three sets of "digits already used" -- one per row, one
# per column, one per box -- updated incrementally as digits are
# placed and removed. This turns each validity check into an O(1)
# set lookup instead of an O(n) scan, which is what actually matters
# for passing within LeetCode's time limit on this problem.
n = 9
# sc: O(n), one set per row tracking digits already placed in that row
rows = [set() for _ in range(n)]
# sc: O(n), one set per column tracking digits already placed in that column
cols = [set() for _ in range(n)]
# sc: O(n), one set per 3x3 box tracking digits already placed in that box
boxes = [set() for _ in range(n)]
# sc: O(m), list of empty cell coordinates to fill, m = number of empty cells
empties = []
# Preprocess board: seed rows/cols/boxes sets and collect empty cells
# tc: O(n^2), fixed 81-cell scan regardless of input
for r in range(n):
for c in range(n):
digit = board[r][c]
if digit == '.':
empties.append((r, c))
else:
b = (r // 3) * 3 + c // 3
rows[r].add(digit)
cols[c].add(digit)
boxes[b].add(digit)
def dfs(idx: int) -> bool:
# New Valid board:
# every empty cell has been filled validly
if idx == len(empties):
return True
row, col = empties[idx]
b = (row // 3) * 3 + col // 3
# Path 1:
# try every digit 1-9 in this empty cell
# tc: O(9) per call, bounded by fixed digit range
for digit in "123456789":
# Early Pruning:
# O(1) set lookups instead of Solution 1's O(n) row/col/box scan
if digit in rows[row] or digit in cols[col] or digit in boxes[b]:
continue
# Choose:
# place digit on board and mark it used in all three constraint sets
board[row][col] = digit
rows[row].add(digit)
cols[col].add(digit)
boxes[b].add(digit)
# Explore:
# recurse to the next empty cell, if this eventually completes
# the whole board, propagate success back up immediately
if dfs(idx + 1):
return True
# Backtrack:
# digit didn't lead to a solution, revert cell and all three
# constraint sets, then try the next digit via the for loop
board[row][col] = '.'
rows[row].discard(digit)
cols[col].discard(digit)
boxes[b].discard(digit)
# Exhausted: no digit works at this cell, this branch has failed
return False
# Start backtracking at the first empty cell, mutates board in place
dfs(0)
# overall: tc O(9^m), m = number of empty cells, each check now O(1) instead of O(n)
# overall: sc O(m), recursion stack depth + rows/cols/boxes sets
return