Backtracking
Build an answer one choice at a time. If a choice leads to a dead end, take it back and try the next one. It's a systematic "try everything" that never wastes time on a path it already knows is hopeless.
1. What is this pattern for?
Backtracking is how you generate every valid arrangement: all subsets, all orderings, all combinations that hit a target, every way to place queens on a board, or every path that spells a word. It's a depth-first search over a decision tree:
- Each node is a partial answer (the "path so far").
- Each edge is one choice you could add next.
- Each leaf is either a complete answer (record it) or a dead end (abandon it).
The magic word is pruning. As soon as a partial answer breaks a rule (queens attack each other, the sum is already too big), you stop exploring that whole branch. Pruning is what turns "impossible" into "fast enough".
2. How it works (bisected)
- Define the path. What does a partial answer look like? Usually a list
path, plus a pointer such asstartorithat says where the next choices come from. - Define "done". When is
patha full answer? (length = n, remaining target = 0, row = n, all letters matched.) When it is, append a copy:results.append(path[:]). - List the choices. From this node, which items may come next? Subsets/combinations: only items at index ≥
start, so you never produce[2,1]after[1,2]. Permutations: every item that isn'tusedyet. - Prune. Skip any choice that already breaks a rule. Sort first so you can
breakinstead ofcontinueonce values get too big. - Choose → explore → un-choose.
path.append(x), recurse,path.pop(). Undo every change you made (sets, used flags, board marks) in exact reverse order. - Duplicates? If the input has repeated values, sort it and skip
nums[i] == nums[i-1]wheni > start. Never try the same value twice at the same depth.
3. Seeing a problem: how to spot Backtracking
Trigger words
- "return all possible…", "list every…", "generate all"
- "subsets", "power set", "permutations", "combinations", "partitions"
- "place N queens", "solve the Sudoku", "fill the board"
- "does a path exist that spells the word, without reusing a cell?"
- "letter combinations", "generate valid parentheses", "IP addresses"
Shape of the input
- Small n, typically
n ≤ 10…20. The constraints are a big hint that exponential time is expected. - The output is a list of lists/strings, whose size itself is exponential.
- Brute-force smell: "nested loops, but the number of loops depends on the input". Recursion gives you a variable number of nested loops.
4. When to use it, and when not to
| Use Backtracking when… | Prefer something else when… |
|---|---|
| You must output all subsets / permutations / combinations | You only need the number of ways, or the min/max: use Dynamic Programming (e.g. Coin Change II instead of listing combinations) |
| Constraint satisfaction: N-Queens, Sudoku, word search without reusing cells | You only need to reach cells and never un-visit them: plain flood fill (Matrix Traversal / DFS) |
| Strong pruning cuts most branches early | You need the shortest sequence of moves: BFS |
| n is small (≤ ~20) | The answer is a contiguous slice of an array: Sliding Window / Two Pointers, which run in O(n) |
Cost (the output size dominates):
| Problem family | Leaves in the tree | Time | Extra space (recursion) |
|---|---|---|---|
| Subsets | 2ⁿ | O(n·2ⁿ) | O(n) |
| Permutations | n! | O(n·n!) | O(n) |
| Combinations C(n,k) | C(n,k) | O(k·C(n,k)) | O(k) |
| N-Queens | ≤ n! | O(n!) (heavily pruned) | O(n) |
| Word Search (word length L) | ≤ 4·3L−1 per start | O(R·C·3L) | O(L) |
5. The Python template (memorize this)
# The universal shape.
# It is filled in here for "all combinations of k numbers" so you can run it;
# for your own problem, swap in your own is_done / is_valid tests.
def is_done(path, k):
# 1) goal test: here, "the path already holds k numbers"
return len(path) == k
def is_valid(choice):
# 3) pruning test: here every number is allowed (real problems reject bad choices)
return True
def solve(nums, k):
results = [] # every finished answer we find
path = [] # the choices made so far on the current branch
solve_helper(nums, k, 0, path, results)
return results
def solve_helper(nums, k, start, path, results):
# 'start' (or index / row / used) = where the next choices come from
if is_done(path, k): # 1) goal reached?
results.append(path[:]) # record a COPY (path keeps changing later)
return
for i in range(start, len(nums)): # 2) try every candidate choice
if not is_valid(nums[i]): # 3) prune bad branches early
continue
path.append(nums[i]) # 4) choose
solve_helper(nums, k, i + 1, path, results) # 5) explore (i + 1: no reuse, i: reuse allowed)
path.pop() # 6) un-choose (undo EXACTLY what you did)
# Variant A: subsets (record at EVERY node, not just at the leaves)
def subsets(nums):
results = []
path = []
subsets_helper(nums, 0, path, results)
return results
def subsets_helper(nums, start, path, results):
results.append(path[:]) # every node of the tree is a valid subset
for i in range(start, len(nums)):
path.append(nums[i]) # choose
subsets_helper(nums, i + 1, path, results) # explore
path.pop() # un-choose
# Variant B: permutations (order matters, so use 'used' flags instead of 'start')
def permutations(nums):
results = []
path = []
used = [False] * len(nums) # used[i] is True while nums[i] sits in path
permutations_helper(nums, used, path, results)
return results
def permutations_helper(nums, used, path, results):
if len(path) == len(nums): # every number placed: one full ordering
results.append(path[:])
return
for i in range(len(nums)): # look at ALL positions, not just later ones
if used[i]:
continue # already in the path, skip it
used[i] = True # choose (two changes...)
path.append(nums[i])
permutations_helper(nums, used, path, results) # explore
path.pop() # un-choose (...so undo both)
used[i] = False
# Variant C: input has duplicates, so sort + skip equal siblings
def subsets_with_dup(nums):
nums.sort() # equal values now sit next to each other
results = []
path = []
subsets_with_dup_helper(nums, 0, path, results)
return results
def subsets_with_dup_helper(nums, start, path, results):
results.append(path[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue # same value already tried at this depth
path.append(nums[i])
subsets_with_dup_helper(nums, i + 1, path, results)
path.pop()
What the code is doing, line by line:
- The universal shape is split into a starter function
solveand a recursive helpersolve_helper. The starter creates the two lists every backtracking solution needs:results(finished answers) andpath(the choices on the branch we are walking right now), then makes the first call withstart = 0. is_doneandis_validare the two problem-specific questions. Here they are filled in for "pick k numbers" so the code actually runs; in a new problem you rewrite just these two.- Inside the helper, step 1 asks is the path a finished answer? If yes we append
path[:], a copy, becausepathitself will keep being changed by later appends and pops. - Step 2 loops over every candidate from
startonward. Step 3 skips (prunes) candidates that can never lead to an answer, so their whole subtree is never explored. - Steps 4–6 are the heart of backtracking:
append(choose), recursive call (explore),pop(un-choose). After the pop,pathis exactly as it was before, ready for the next candidate. - Variant A (subsets) has no goal test: it appends a copy at the start of every call, because every node of the tree (not just the leaves) is a valid subset.
- Variant B (permutations) loops over all indices from 0 (order matters, so
[2,1]must be reachable) and uses theusedlist to avoid picking the same index twice. It changes two things when choosing, so it undoes two things when un-choosing. - Variant C (duplicates) sorts first, then skips
nums[i]when it equals the value just tried at the same depth (i > start). This stops two identical branches from producing the same subset twice.
Dry run on the example:
| Call | path | What happens | results |
|---|---|---|---|
solve([1,2,3], 2) | [] | start = 0, try 1 | [] |
| helper(start=1) | [1] | not done (1 item), try 2 | [] |
| helper(start=2) | [1,2] | done: save copy, return | [[1,2]] |
| back in start=1 | [1] | pop 2, try 3 | [[1,2]] |
| helper(start=3) | [1,3] | done: save copy | [[1,2],[1,3]] |
| back in start=0 | [] | pop 3, pop 1, try 2 | same |
| helper(start=2) | [2] | try 3 → [2,3] done: save | [[1,2],[1,3],[2,3]] |
| back in start=0 | [] | try 3 → [3], no numbers left, nothing saved | final answer |
6. How to remember it forever
append has a matching pop. Every add has a matching remove.How this hook works: what “Chalk in the maze” means
What the slogan means, phrase by phrase
- “Chalk in the maze”
- You explore a tree of choices one path at a time, keeping marks of the path you are on.
- “Draw the arrow (choose)”
path.append(choice).- “walk (explore)”
- Recurse with that choice made.
- “rub it out on the way back (un-choose)”
path.pop(), which undoes exactly what you did so the next branch starts clean.- “Every append has a matching pop”
- The single shared
pathmust be identical before and after each call. That's what lets one list serve every branch.
Why this picture fits the pattern: Backtracking problems ask for all subsets, permutations or arrangements. You can't build them all at once, so you walk every path of choices and undo each step on the way back.
The story → the code
| In the story | In the code |
|---|---|
| A fork | for i in range(start, len(nums)) |
| Draw the arrow | path.append(nums[i]) |
| Walk on | solve_helper(nums, k, i + 1, path, results) |
| Rub it out | path.pop() |
| Found an exit | is_done → results.append(path[:]) |
| A rotten branch | is_valid fails → continue (pruning) |
Watch it on numbers
All combinations of 2 numbers from [1, 2, 3].
- Chalk 1 → [1]. Chalk 2 → [1, 2]: an exit, so record it. Rub out 2 → [1].
- Chalk 3 → [1, 3]: record. Rub out 3, then rub out 1 → [].
- Chalk 2 → [2]. Chalk 3 → [2, 3]: record. Rub out both → [].
- Chalk 3 → [3]. No forks left, so rub out → []. Results
[[1, 2], [1, 3], [2, 3]].
The trap it saves you from
results.append(path) without a copy. You store the same list three times, and after all the rubbing out every entry is []. Always record path[:]. Forgetting pop() makes the path grow forever.
It still works when…
Permutations: the chalk is a used set, add on choose and remove on un-choose. N-Queens: place a queen (mark its column and diagonals), recurse, then remove her. "Combos look forward (start), perms look around (used)."
Picture
An upside-down tree. You're a squirrel running down one branch at a time. At a leaf you either grab a nut (record the answer) or find nothing. Then you climb back up to the last fork and try the next branch. Branches you can see are rotten (pruning), you skip entirely.
Chant
"Done? Copy. Loop. Prune. Choose, explore, un-choose."
Start vs used
"Combos look forward (start), perms look around (used)." Reuse allowed? Recurse with i; otherwise i + 1.
7. LeetCode problems, solved step by step
Medium 78. Subsets Start here Google Meta Amazon Microsoft
Problem: Given distinct integers, return every possible subset (the power set), in any order.
Solve it on LeetCode ↗ · Video walkthrough ↗
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.
Hint 1
Every element is either in the subset or not.
Hint 2
Backtrack: record the current path at every node of the recursion, then try adding each later element.
From brute force to the pattern: There are 2ⁿ subsets, so any solution is at least O(n · 2ⁿ) to output them. Backtracking (or iterating over bitmasks) generates each subset exactly once without duplicates.
Spot it: "all possible subsets" plus n ≤ 10. The output alone has 2ⁿ items, so exponential time is unavoidable and expected.
Key insight: Each element gives a yes/no decision: take it or skip it. That's a binary decision tree of depth n with 2ⁿ leaves, and each leaf is one subset. The first visualizer above draws exactly this tree.
Approach (step by step):
backtrack(index)decides aboutnums[index]. The path so far holds the chosen numbers.- If
index == len(nums), every number has been decided, so record a copy ofpath. - Take branch:
path.append(nums[index]), recurse withbacktrack(index + 1), thenpath.pop()to undo. - Skip branch: recurse with
backtrack(index + 1)without adding anything. - Start with
backtrack(0)and returnresults.
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
self.nums = nums
self.path = [] # numbers chosen so far on the current branch
self.results = [] # every finished subset
self.backtrack(0)
return self.results
def backtrack(self, index):
# 'index' = which number we are deciding about right now (take it or skip it)
if index == len(self.nums):
# decided about every number: this branch is a leaf, save a COPY of path
self.results.append(self.path[:])
return
# Branch 1: take nums[index]
self.path.append(self.nums[index])
self.backtrack(index + 1)
self.path.pop() # undo the take before trying the other branch
# Branch 2: skip nums[index] (path is unchanged)
self.backtrack(index + 1)
What the code is doing, line by line:
subsetsstores the input and two lists onselfso the helper method can reach them:path(numbers taken on the current branch) andresults(finished subsets). Then it starts the decision tree atindex = 0.backtrack(index)makes one yes/no decision aboutnums[index].- If
index == len(self.nums)every number has been decided, so this is a leaf: saveself.path[:](a copy) and return. - Branch 1 (take): append the number, recurse to the next index, then
popit so the path is clean again. - Branch 2 (skip): recurse to the next index without adding anything.
- Each number doubles the number of branches, so 3 numbers give 2 × 2 × 2 = 8 leaves = 8 subsets.
Dry run on the example:
| Call | path | Action | results so far |
|---|---|---|---|
backtrack(0) | [] | take 1 | – |
backtrack(1) | [1] | take 2 | – |
backtrack(2) | [1,2] | take 3 | – |
backtrack(3) | [1,2,3] | leaf: save | [1,2,3] |
back in backtrack(2) | [1,2] | pop 3, skip 3 → leaf: save | + [1,2] |
back in backtrack(1) | [1] | pop 2, skip 2, then take 3 → leaf | + [1,3] |
[1] | pop 3, skip 3 → leaf | + [1] | |
back in backtrack(0) | [] | pop 1, skip 1: same pattern on [2,3] | + [2,3], [2], [3], [] |
| done | 8 subsets | [[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]] |
Edge cases to test:
- An empty array →
[[]]. - A single element →
[[], [x]]. - Negative numbers (they don't matter).
Complexity: O(n·2ⁿ) time (2ⁿ subsets, each copied in O(n)), O(n) recursion depth.
Medium 46. Permutations Start here Google Meta Amazon Cisco Microsoft
Problem: Return every ordering of a list of distinct integers.
Solve it on LeetCode ↗ · Video walkthrough ↗
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]]Constraints: 1 ≤ nums.length ≤ 6 · -10 ≤ nums[i] ≤ 10 · All the integers of nums are unique.
Hint 1
Build a permutation one position at a time.
Hint 2
At each step, try every unused number (track used), recurse, then undo the choice.
From brute force to the pattern: There are n! permutations, so the output alone is O(n · n!). Backtracking with a used array produces each permutation once.
Spot it: "all possible permutations". Order matters, so [1,2] and [2,1] both count. A start index would lose orderings. Use used flags.
Key insight: At depth d you pick which unused number goes in position d. That gives n choices, then n−1, then n−2… for n! leaves. The second visualizer above shows the tree growing.
Approach (step by step):
- Keep
pathand ausedflag per index. - If
pathis as long asnums, record a copy. - Loop over all indices (order matters, so there's no
start) and skip those already used. - Choose: mark used + append. Explore: recurse.
- Un-choose: pop and unmark. Undo both changes.
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
self.nums = nums
self.path = [] # the ordering built so far
self.results = [] # every complete ordering
self.used = [False] * len(nums) # used[i] is True while nums[i] is in path
self.backtrack()
return self.results
def backtrack(self):
if len(self.path) == len(self.nums):
# every number has a position: save a COPY of this ordering
self.results.append(self.path[:])
return
# order matters, so look at EVERY index (no 'start')
for i in range(len(self.nums)):
if self.used[i]:
continue # already placed earlier in this path
number = self.nums[i]
self.used[i] = True # choose: mark it used...
self.path.append(number) # ...and put it in the next position
self.backtrack() # explore
self.path.pop() # un-choose: undo BOTH changes
self.used[i] = False
What the code is doing, line by line:
permutesets uppath,resultsand ausedlist ofFalseflags ([False] * nmakes a list of n copies ofFalse), then starts the search.backtrack()fills the next free position. Whenpathis as long asnums, the ordering is complete, so a copy is saved.- The loop looks at every index from 0, because a number further left may still go into a later position. Indices already used on this path are skipped.
- Choosing changes two things: the flag
used[i] = Trueandpath.append. After the recursive call returns, both are undone in reverse order so the next loop iteration starts clean.
Dry run on the example:
| Depth | path | used | Action | results |
|---|---|---|---|---|
| 0 | [] | F F F | choose 1 | – |
| 1 | [1] | T F F | skip 1 (used), choose 2 | – |
| 2 | [1,2] | T T F | choose 3 | – |
| 3 | [1,2,3] | T T T | full: save | [1,2,3] |
| 1 | [1] | T F F | undo 3, undo 2; choose 3 | |
| 2 | [1,3] | T F T | choose 2 → full: save | + [1,3,2] |
| 0 | [] | F F F | undo everything; choose 2, then 3 | + [2,1,3], [2,3,1] |
| 0 | [] | F F F | choose 3 | + [3,1,2], [3,2,1] (6 total) |
Edge cases to test:
- A single element.
- Negative numbers.
- (All numbers are distinct here; with duplicates see Permutations II.)
Complexity: O(n·n!) time, O(n) extra space.
Medium 39. Combination Sum Start here Google Meta Amazon Microsoft
Problem: Given distinct positive candidates and a target, list every unique combination that sums to target. You may reuse a number any number of times.
Solve it on LeetCode ↗ · Video walkthrough ↗
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]]Constraints: 1 ≤ candidates.length ≤ 30 · 2 ≤ candidates[i] ≤ 40 · All elements of candidates are distinct. · 1 ≤ target ≤ 40
Hint 1
You may reuse a number, but [2, 3] and [3, 2] count as the same combination.
Hint 2
Backtrack with a start index: after choosing candidates[i], recurse from i (not i + 1) so it can be reused. Stop when the remaining sum is negative.
From brute force to the pattern: Generating all sequences up to the target and de-duplicating is wasteful. Choosing in non-decreasing index order avoids duplicates, and pruning on a negative remainder (faster after sorting) cuts dead branches early.
Spot it: "all unique combinations" + "sum to target". Combinations (order doesn't matter), so use start. Reuse is allowed, so recurse with i, not i+1.
Key insight: Track the remaining amount. Sort first: the moment a candidate exceeds the remainder, every later one does too, so you can break. That's powerful pruning.
Approach (step by step):
- Sort the candidates so you can stop early.
backtrack(start, remaining): ifremaining == 0, record a copy of the path.- Loop
ifromstart. Ifcandidates[i] > remaining,break: every later candidate is bigger still. - Append the candidate and recurse with
backtrack(i, remaining − number). Passingi(noti+1) allows reuse. - Pop to undo.
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() # sorted, so we can stop as soon as a number is too big
self.candidates = candidates
self.path = [] # numbers picked so far
self.results = [] # combinations that hit the target exactly
self.backtrack(0, target)
return self.results
def backtrack(self, start, remaining):
# 'remaining' = how much we still need to add to reach the target
if remaining == 0:
self.results.append(self.path[:]) # exact hit: save a COPY
return
for i in range(start, len(self.candidates)):
number = self.candidates[i]
if number > remaining:
break # sorted: this and every later number is too big
self.path.append(number) # choose
self.backtrack(i, remaining - number) # explore; i (not i + 1): reuse allowed
self.path.pop() # un-choose
What the code is doing, line by line:
- Sort the candidates, store them on
self, and start with the full target stillremaining. - If
remaining == 0the path sums exactly to the target, so a copy is saved. - The loop starts at
start, so we never go back to smaller indices. That is what keeps[2,3]and[3,2]from both appearing. - Because the list is sorted, the first
number > remainingmeans all later ones are too big as well, so webreakout of the loop instead of just skipping one. - We recurse with
i(noti + 1), so the same number can be picked again, and withremaining - numberbecause we just used up that much. Thenpopundoes the choice.
Dry run on the example:
| Call (start, remaining) | path | Action | results |
|---|---|---|---|
| (0, 7) | [] | pick 2 | – |
| (0, 5) | [2] | pick 2 | – |
| (0, 3) | [2,2] | pick 2 | – |
| (0, 1) | [2,2,2] | 2 > 1: break, return | – |
| (0, 3) | [2,2] | pop; pick 3 | – |
| (1, 0) | [2,2,3] | remaining 0: save | [2,2,3] |
| (0, 3) | [2,2] | pop; 6 > 3: break | |
| (0, 5) | [2] | pick 3 → (1, 2): 3 > 2 break; then 6 > 5 break | |
| (0, 7) | [] | pick 3 → (1, 4): pick 3 → (1, 1) breaks; 6 > 4 break | |
| (0, 7) | [] | pick 6 → (2, 1): break | |
| (0, 7) | [] | pick 7 → (3, 0): save | [[2,2,3],[7]] |
Edge cases to test:
- No combination exists →
[]. - A candidate equal to the target.
- A target smaller than every candidate.
Complexity: exponential, roughly O(N^(T/m)) where T = target and m = smallest candidate. Recursion depth is at most T/m.
Hard 51. N-Queens Google Meta Amazon Microsoft
Problem: Place n queens on an n × n board so that no two share a row, column or diagonal. Return every such board.
n = 4[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]n = 1[["Q"]]Constraints: 1 ≤ n ≤ 9
Hint 1
Place exactly one queen per row; for each row, choose a column.
Hint 2
Track used columns and both diagonals (r − c and r + c) in sets so each safety check is O(1).
From brute force to the pattern: Trying every placement of n queens on n² squares is astronomically large. One queen per row with O(1) column/diagonal checks prunes almost everything: about O(n!).
Spot it: "return all distinct solutions" + placement constraints. That's classic constraint-satisfaction backtracking.
Key insight: Put exactly one queen per row, so the row is the depth of the tree. For O(1) conflict checks, keep three sets: used columns, used diagonals (r − c is constant along ↘), and used anti-diagonals (r + c is constant along ↙).
Approach (step by step):
- Place exactly one queen per row and recurse row by row, so the row is the depth.
- Keep three sets: used columns, used
row − col(↘ diagonals) and usedrow + col(↙ anti-diagonals). - In row
row, try every columncol. Skip it if any set already contains its key (attacked → prune). - Place the queen: add to all three sets, remember the column, recurse to
row + 1. - Remove the queen from all three sets (un-choose) and try the next column.
- When
row == n, build the board strings from the remembered columns and record them.
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
self.n = n
self.results = []
self.queen_col = [-1] * n # queen_col[row] = column of the queen in that row (-1 = none yet)
self.used_columns = set() # columns that already hold a queen
self.used_diagonals = set() # row - col is the same along a down-right diagonal
self.used_anti_diagonals = set() # row + col is the same along a down-left diagonal
self.backtrack(0)
return self.results
def backtrack(self, row):
if row == self.n:
# a queen sits in every row: turn the columns into board strings
board = []
for r in range(self.n): # r = row number
col = self.queen_col[r]
row_text = "." * col + "Q" + "." * (self.n - col - 1)
board.append(row_text)
self.results.append(board)
return
for col in range(self.n): # try every column in this row
diagonal = row - col
anti_diagonal = row + col
if col in self.used_columns:
continue # attacked from above in the same column
if diagonal in self.used_diagonals or anti_diagonal in self.used_anti_diagonals:
continue # attacked along a diagonal
# choose: place the queen and remember everything it attacks
self.used_columns.add(col)
self.used_diagonals.add(diagonal)
self.used_anti_diagonals.add(anti_diagonal)
self.queen_col[row] = col
self.backtrack(row + 1) # explore the next row
# un-choose: lift the queen so the next column can be tried
self.used_columns.remove(col)
self.used_diagonals.remove(diagonal)
self.used_anti_diagonals.remove(anti_diagonal)
self.queen_col[row] = -1
What the code is doing, line by line:
solveNQueenspreparesqueen_col(one slot per row,-1= empty) and three sets that answer "is this square attacked?" instantly: used columns, usedrow - coldiagonals and usedrow + colanti-diagonals.backtrack(row)places the queen for one row. Ifrow == n, every row has a queen, so we build the board: for each row,coldots, aQ, then the remaining dots.- Otherwise we try every column. A column is skipped when it, its diagonal, or its anti-diagonal is already in a set: that square is attacked, so the whole subtree below it is pruned.
- To place the queen we add its three keys to the sets and record its column, then recurse to
row + 1. - When the call returns we remove exactly those three keys and reset
queen_col[row], so the board is as it was before and the next column can be tried.
Dry run on the example:
| row | Try col | Check (row-col, row+col) | Action |
|---|---|---|---|
| 0 | 0 | free | place; cols {0}, diag {0}, anti {0} |
| 1 | 0, 1, 2 | col 0 used; diag 0 used; (−1, 3) free | place at col 2 |
| 2 | 0..3 | col used / anti 3 used / col used / diag −1 used | dead end: lift (1,2) |
| 1 | 3 | (−2, 4) free | place at col 3 |
| 2 | 0, 1 | col 0 used; (1, 3) free | place at col 1 |
| 3 | 0..3 | every column attacked | dead end: lift (2,1); col 2, 3 also fail; lift (1,3), lift (0,0) |
| 0 | 1 | free | place; cols {1}, diag {−1}, anti {1} |
| 1 | 3 | (−2, 4) free (cols 0..2 attacked) | place |
| 2 | 0 | (2, 2) free | place |
| 3 | 2 | (1, 5) free | place → row 4 == n: save [".Q..","...Q","Q...","..Q."] |
(Traced on n = 4 until the first solution; the search continues the same way and finds the mirror-image board too.)
Edge cases to test:
n = 1→ one board.n = 2orn = 3→ no solutions.- Building the output strings correctly.
Complexity: O(n!) time in the worst case (far less in practice thanks to pruning), O(n) space for the sets and recursion. n = 8 has 92 solutions.
Medium 79. Word Search Google Meta Amazon Cisco Microsoft Netflix
Problem: Given a letter grid and a word, return whether the word can be traced through 4-directionally adjacent cells, using each cell at most once.
Solve it on LeetCode ↗ · Video walkthrough ↗
board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"trueboard = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"trueConstraints: 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.
Hint 1
Start a DFS from every cell that matches the first letter.
Hint 2
Mark a cell as used while it's on the current path (e.g. replace it with #), and restore it when you backtrack.
From brute force to the pattern: Trying every path in the grid is exponential no matter what, but backtracking prunes as soon as a letter doesn't match: O(r · c · 3^L).
Spot it: it looks like grid traversal, but "each cell at most once per path" means a cell used by one attempt must be available again to the next attempt. So you must un-mark it when you back out. That's backtracking.
Key insight: DFS from each cell with index k into the word. Mark the cell with '#' while it's on the current path and restore it afterwards. Return early on the first success.
Approach (step by step):
- Try every cell as the starting point of the word.
dfs(row, col, index): ifindex == len(word), every letter matched, so return True.- If
(row, col)is out of bounds or its letter isn'tword[index], return False. - Choose: overwrite the cell with
'#'so this path can't reuse it, then try the 4 neighbors withindex + 1. - Un-choose: restore the original letter (even on success), then return the result.
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
self.board = board
self.word = word
self.rows = len(board)
self.cols = len(board[0])
# the 4 neighbours as [row change, column change]: down, up, right, left
self.directions = [[1, 0], [-1, 0], [0, 1], [0, -1]]
# try every cell as the place where the word starts
for row in range(self.rows):
for col in range(self.cols):
if self.dfs(row, col, 0):
return True
return False
def dfs(self, row, col, index):
# 'index' = which letter of the word this cell must match
if index == len(self.word):
return True # matched every letter
if row < 0 or row >= self.rows or col < 0 or col >= self.cols:
return False # stepped off the board
if self.board[row][col] != self.word[index]:
return False # wrong letter (a cell on this path holds '#')
saved_letter = self.board[row][col]
self.board[row][col] = "#" # choose: occupy this cell for the current path
found = False
for direction in self.directions:
next_row = row + direction[0]
next_col = col + direction[1]
if self.dfs(next_row, next_col, index + 1):
found = True
break # one success is enough
self.board[row][col] = saved_letter # un-choose: free the cell again (even on success)
return found
What the code is doing, line by line:
existstores the board, the word and the board size onself, lists the four neighbour directions, and tries every cell as a starting point. The first success returnsTrue.dfs(row, col, index)asks: can the word, from letterindexonward, be traced starting at this cell?- If
index == len(word), all letters were matched by earlier cells, so the answer isTrue. - If the cell is off the board, or holds a different letter, this path fails. A cell marked
'#'never equals a letter, so a path can't reuse its own cells. - Choose: remember the letter and overwrite the cell with
'#'. Explore: try the four neighbours withindex + 1, stopping at the first success. - Un-choose: put the letter back no matter what, so other starting points and other paths can use this cell.
Dry run on the example:
| Call (row, col, index) | Cell | Needs | Result |
|---|---|---|---|
| (0,0,0) | A | A | match: mark #, try neighbours |
| (1,0,1) down | S | B | False |
| (−1,0,1) up | off board | B | False |
| (0,1,1) right | B | B | match: mark, try neighbours |
| (1,1,2) down | F | C | False (up is off board) |
| (0,2,2) right | C | C | match: mark |
| (1,2,3) down | C | C | match: mark |
| (2,2,4) down | E | E | match: mark |
| (3,2,5) / (1,2,5) / (2,3,5) | off / # / E | D | False, False, False |
| (2,1,5) left | D | D | match: mark |
| (3,1,6) down | index 6 == len(word): True | ||
| unwind | every cell restored, exist returns True |
(Board ABCE / SFCS / ADEE, word "ABCCED".)
Edge cases to test:
- The word is longer than the number of cells.
- The same cell can't be used twice, e.g.
"ABA"on a 1 × 2 grid. - A single-letter word.
Complexity: O(R·C·3L) time (after the first step, each cell has at most 3 unvisited directions), O(L) recursion depth. Interview bonus: prune early if the board's letter counts can't cover the word.
8. Common mistakes
path instead of path[:]Every entry in results would be the same list object, and after all the pops it's empty. You'd return [[], [], [], …]. Always copy.used, cols, or mark the board, you must undo it after the recursive call. A missing undo silently blocks valid answers later.i vs i + 1 vs startRecurse with i + 1 → each element used at most once. With i → unlimited reuse. Looping from 0 with no start → you get permutations (duplicates like [2,3] and [3,2]).i > start and nums[i] == nums[i-1], and only after sorting. Using i > 0 wrongly skips taking a duplicate deeper in the same path (it would lose [2,2]).break often makes the difference between TLE and accepted.9. Self-check quiz
Inside the base case, which line correctly records the answer?
path keeps changing as you backtrack. You need a snapshot copy."Return the number of ways to make amount 500 from coins [1, 2, 5]."
In Combination Sum, each candidate may be reused. Which recursive call is right?
i allows reuse. i + 1 forbids it. Restarting at 0 creates permutations of the same combination.N-Queens: which value is identical for every square on the same ↘ diagonal?
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 1863 | Sum of All Subset XOR Totals | Easy | Warm-up: take/skip tree | |
| 78 | Subsets | Medium | Record at every node (core) | |
| 17 | Letter Combinations of a Phone Number | Medium | Choices come from a digit → letters map | |
| 46 | Permutations | Medium | used flags instead of start | |
| 39 | Combination Sum | Medium | Reuse → recurse with i, sort + break | |
| 90 | Subsets II | Medium | Duplicates: sort + skip equal siblings | |
| 40 | Combination Sum II | Medium | No reuse + duplicates | |
| 22 | Generate Parentheses | Medium | Prune with open/close counts | |
| 131 | Palindrome Partitioning | Medium | Choice = where to cut next | |
| 79 | Word Search | Medium | Mark / unmark grid cells | |
| 77 | Combinations | Medium | Subsets with a fixed size: only record paths of length k | |
| 47 | Permutations II | Medium | Sort; at each position skip a value equal to the previous one unless that one is in use | |
| 784 | Letter Case Permutation | Medium | Each letter branches into lower/upper; digits don't branch | |
| 1291 | Sequential Digits | Medium | Every answer is a substring of "123456789": try each length and start | |
| 1415 | The k-th Lexicographical String of All Happy Strings of Length n | Medium | The first letter has 3 choices and each later letter 2: pick letters by dividing k by the block sizes | |
| 1239 | Maximum Length of a Concatenated String with Unique Characters | Medium | Backtrack over the strings with a 26-bit mask of used letters; skip strings that clash | |
| 93 | Restore IP Addresses | Medium | Backtrack over 4 parts of 1–3 digits; prune parts > 255, leading zeros, and lengths that can't fit | |
| 89 | Gray Code | Medium | Reflect and prefix: or directly i XOR (i >> 1) | |
| 698 | Partition to K Equal Sum Subsets | Medium | Backtrack placing big numbers first into k buckets; skip buckets with a load already tried | |
| 1980 | Find Unique Binary String | Medium | Cantor's diagonal: make bit i differ from string i's bit i | |
| 216 | Combination Sum III | Medium | Backtrack over digits 1–9 in increasing order; stop when the size or sum overshoots | |
| 526 | Beautiful Arrangement | Medium | Fill positions one by one, trying unused numbers that divide or are divided by the position | |
| 51 | N-Queens | Hard | 3 sets for O(1) attack checks | |
| 37 | Sudoku Solver | Hard | Stop at the first solution (return True up the stack) | |
| 212 | Word Search II | Hard | Backtracking + Trie pruning | |
| 60 | Permutation Sequence | Hard | Each leading digit owns a block of (n−1)! permutations: pick digits by dividing k | |
| 679 | 24 Game | Hard | Pick any two numbers, replace them with each result of + − × ÷, recurse until one number is left | |
| 1240 | Tiling a Rectangle with the Fewest Squares | Hard | Skyline backtracking: always fill the lowest, leftmost gap with the largest square first; prune by the best so far | |
| 1723 | Find Minimum Time to Finish All Jobs | Hard | Assign jobs largest first; prune when a worker exceeds the best, and skip workers with an equal load | |
| 3348 | Smallest Divisible Digit Product II | Hard | Find the rightmost position you can raise so the remaining factors of t fit in the free suffix | |
| 301 | Remove Invalid Parentheses | Hard | Count the extra '(' and ')' to remove, then backtrack removing exactly those, skipping duplicate choices | |
| 489 | Robot Room Cleaner | Hard | Backtracking DFS in coordinates you invent; after each move, turn around, move back, turn around to undo it | |
| 980 | Unique Paths III | Hard | Backtracking DFS that must step on every empty cell exactly once before reaching the end | |
| 140 | Word Break II | Hard | Backtrack over prefixes that are words; memoise the sentences for each suffix start |
Worked solutions for the practice list
Every problem in the table above that isn't already solved in section 7 is worked out here, so you never need to leave this page. Click a problem name in the table to jump to its solution.
Easy 1863. Sum of All Subset XOR Totals
Problem: The XOR total of a subset is the XOR of all its elements (0 for the empty subset). Return the sum of the XOR totals over every subset of nums.
nums = [1,3]6nums = [5,1,6]28Constraints: 1 ≤ nums.length ≤ 12 · 1 ≤ nums[i] ≤ 20
Hint 1
Visit every subset and add up their XORs.
Hint 2
Backtrack with two choices per element (take it or skip it), carrying the running XOR.
From brute force to the pattern: Enumerating all 2ⁿ subsets is fine for n ≤ 12. (A neat trick: the answer is (OR of all numbers) × 2ⁿ⁻¹.)
Spot it: "every subset" with a tiny n (≤ 12) is the take/skip decision tree.
Approach (step by step):
dfs(index, xor_so_far):xor_so_faris the XOR of the numbers chosen so far.- If
index == len(nums), we've reached a leaf (one complete subset), so returnxor_so_far. - Otherwise return
dfs(index + 1, xor_so_far ^ nums[index])(take) +dfs(index + 1, xor_so_far)(skip). - There's no explicit un-choose:
xor_so_faris passed as a value, so each branch gets its own copy automatically.
from typing import List
class Solution:
def subsetXORSum(self, nums: List[int]) -> int:
self.nums = nums
return self.dfs(0, 0)
def dfs(self, index, xor_so_far):
# 'xor_so_far' = XOR of the numbers taken so far on this branch
if index == len(self.nums):
return xor_so_far # leaf: the XOR total of one complete subset
# ^ is XOR: it combines two numbers bit by bit (1 where the bits differ)
take_total = self.dfs(index + 1, xor_so_far ^ self.nums[index])
skip_total = self.dfs(index + 1, xor_so_far)
return take_total + skip_total # add up the leaves of both branches
What the code is doing, line by line:
subsetXORSumstoresnumsand starts at index 0 with an XOR of 0 (the empty subset).dfs(index, xor_so_far)decides aboutnums[index]. At a leaf (index == len(nums)) one whole subset is decided, so it returns that subset's XOR total.take_totalexplores the branch that includesnums[index](XOR it in with^);skip_totalexplores the branch that leaves it out.- The function returns the sum of both branches, so the very first call returns the sum over all subsets.
- There is no explicit un-choose:
xor_so_faris a plain number passed by value, so each branch gets its own copy automatically.
Dry run on the example:
| Call (index, xor_so_far) | Meaning | Returns |
|---|---|---|
| (2, 1^3 = 2) | subset {1, 3} | 2 |
| (2, 1) | subset {1} | 1 |
| (1, 1) | took 1: 2 + 1 | 3 |
| (2, 3) | subset {3} | 3 |
| (2, 0) | subset {} | 0 |
| (1, 0) | skipped 1: 3 + 0 | 3 |
| (0, 0) | everything: 3 + 3 | 6 |
(nums = [1, 3]; rows are listed in the order the calls finish.)
Edge cases to test:
- A single element → that element.
- Duplicate values.
- Zeros in the array.
Complexity: O(2ⁿ) time, O(n) recursion depth. (Math shortcut: OR of all numbers × 2ⁿ⁻¹.)
Medium 17. Letter Combinations of a Phone Number Google Meta Amazon Cisco Microsoft Apple
Problem: Each digit 2–9 maps to letters as on a phone keypad. Given a digit string, return every letter string it could spell.
Solve it on LeetCode ↗ · Video walkthrough ↗
digits = "23"["ad","ae","af","bd","be","bf","cd","ce","cf"]digits = "2"["a","b","c"]Constraints: 1 ≤ digits.length ≤ 4 · digits[i] is a digit in the range ['2', '9'].
Hint 1
Each digit maps to 3 or 4 letters, and you need one letter per digit.
Hint 2
Backtrack over the digits: for the current digit, try each of its letters and recurse on the next digit.
From brute force to the pattern: There are up to 4ⁿ combinations, so the output is that size. Backtracking (or iteratively extending a list of prefixes) builds each combination once.
Spot it: "return all possible combinations": one decision per digit, and the choices are that digit's letters.
Approach (step by step):
- If
digitsis empty, return[](an edge case LeetCode tests). - Map each digit to its letters.
backtrack(index): ifindex == len(digits), record''.join(path).- For each letter of
digits[index]: append it, recursebacktrack(index + 1), pop it.
from typing import List
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if len(digits) == 0:
return [] # no digits means no combinations (not [""])
# the letters printed on each phone key
self.phone = {"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"}
self.digits = digits
self.path = [] # letters chosen so far, one per digit
self.results = []
self.backtrack(0)
return self.results
def backtrack(self, index):
# 'index' = which digit we are choosing a letter for
if index == len(self.digits):
# one letter per digit: join the list into a string (this also makes the copy)
self.results.append("".join(self.path))
return
letters = self.phone[self.digits[index]]
for letter in letters: # choices = the letters of the current digit
self.path.append(letter) # choose
self.backtrack(index + 1) # explore the next digit
self.path.pop() # un-choose
What the code is doing, line by line:
- An empty input returns
[]straight away; LeetCode expects that, not[""]. phoneis a dictionary from each digit to its letters.pathcollects one letter per digit.backtrack(index)picks a letter fordigits[index]. Whenindexreaches the end,"".join(path)glues the letters into a new string and saves it.- Otherwise it loops over that digit's letters: append, recurse to the next digit, pop.
Dry run on the example:
| Call | path | Action | results |
|---|---|---|---|
backtrack(0) | [] | digit 2 → try a | – |
backtrack(1) | [a] | digit 3 → try d | – |
backtrack(2) | [a,d] | end: save | ad |
backtrack(1) | [a] | pop d; try e, then f | + ae, af |
backtrack(0) | [] | pop a; try b | + bd, be, bf |
backtrack(0) | [] | pop b; try c | + cd, ce, cf |
(digits = "23", 3 × 3 = 9 strings.)
Edge cases to test:
- An empty input →
[](not[""]). - Digits 7 and 9 have four letters.
- A single digit.
Complexity: O(4ⁿ · n) time (up to 4 letters per digit), O(n) recursion depth.
Medium 90. Subsets II Google Meta Amazon Microsoft
Problem: Like Subsets (78), but nums may contain duplicates. Return all subsets without duplicate subsets.
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
Hint 1
Like Subsets, but the input has duplicates and the output must not.
Hint 2
Sort first; at each level of the recursion, skip a value that's equal to the previous one at that same level.
From brute force to the pattern: Generating every subset and de-duplicating with a set of tuples works but wastes effort. Sorting plus the "skip equal siblings" rule never creates a duplicate in the first place.
Spot it: "all subsets" + "may contain duplicates" calls for the sort + skip-equal-siblings template.
Approach (step by step):
- Sort so equal values sit next to each other.
backtrack(start): record a copy ofpathat every node. Each node is a valid subset.- Loop
ifromstart. Ifi > startandnums[i] == nums[i−1], skip it: that value was already tried at this depth, and would produce the same subsets. - Choose
nums[i], recurse withbacktrack(i + 1), then pop.
from typing import List
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort() # duplicates become neighbours
self.nums = nums
self.path = []
self.results = []
self.backtrack(0)
return self.results
def backtrack(self, start):
self.results.append(self.path[:]) # every node of the tree is a subset
for i in range(start, len(self.nums)):
if i > start and self.nums[i] == self.nums[i - 1]:
continue # same value at the same depth: duplicate branch
self.path.append(self.nums[i]) # choose
self.backtrack(i + 1) # explore; i + 1: each element used at most once
self.path.pop() # un-choose
What the code is doing, line by line:
- Sort first so equal values sit side by side; then store everything on
selfand start at index 0. backtrack(start)saves a copy ofpathimmediately: every node is a valid subset.- The loop offers each later number as the next element. If
i > startand the value equals the one just before it, that value was already offered at this depth, so the branch would repeat subsets: skip it. i > start(noti > 0) matters: the first copy at each depth is still allowed, which is how[2, 2]gets built.- Choose, recurse with
i + 1, pop.
Dry run on the example:
| Call | path saved | Loop | results |
|---|---|---|---|
backtrack(0) | [] | i=0: take 1 | [] |
backtrack(1) | [1] | i=1: take 2 | + [1] |
backtrack(2) | [1,2] | i=2: take 2 | + [1,2] |
backtrack(3) | [1,2,2] | no numbers left | + [1,2,2] |
back in backtrack(1) | i=2: 2 == 2 and i > start: skip | ||
back in backtrack(0) | i=1: take 2 → saves [2], then [2,2] | + [2], [2,2] | |
back in backtrack(0) | i=2: duplicate of i=1: skip | 6 subsets |
(nums = [1, 2, 2].)
Edge cases to test:
- All elements equal, e.g.
[2, 2, 2]. - No duplicates → same as Subsets.
- The empty subset is always included.
Complexity: O(n · 2ⁿ) time, O(n) recursion depth.
Medium 40. Combination Sum II Google Amazon
Problem: Given candidates (may contain duplicates) and target, return every unique combination that sums to target. Each candidate may be used at most once.
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 · 1 ≤ candidates[i] ≤ 50 · 1 ≤ target ≤ 30
Hint 1
Each candidate may be used once, and duplicate combinations aren't allowed.
Hint 2
Sort; recurse from i + 1; skip a candidate equal to the previous one at the same recursion level; stop once a candidate exceeds the remainder.
From brute force to the pattern: Enumerating subsets and filtering by sum with de-duplication is O(2ⁿ) with wasted duplicates. Sorted backtracking with sibling skipping and early stopping explores only distinct combinations.
Spot it: This is Combination Sum (39) with two changes: no reuse (recurse with i + 1) and duplicate inputs (skip equal siblings).
Approach (step by step):
- Sort the candidates.
backtrack(start, remaining): ifremaining == 0, record a copy.- Loop
ifromstart. Skip it ifi > startand it equals the previous value.breakas soon ascandidates[i] > remaining. - Choose it, recurse with
backtrack(i + 1, remaining − number), then pop.
from typing import List
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() # equal values side by side, and lets us break early
self.candidates = candidates
self.path = []
self.results = []
self.backtrack(0, target)
return self.results
def backtrack(self, start, remaining):
if remaining == 0:
self.results.append(self.path[:]) # exact hit: save a COPY
return
for i in range(start, len(self.candidates)):
if i > start and self.candidates[i] == self.candidates[i - 1]:
continue # skip a duplicate sibling (same value, same depth)
number = self.candidates[i]
if number > remaining:
break # sorted: nothing later fits either
self.path.append(number) # choose
self.backtrack(i + 1, remaining - number) # explore; i + 1: each candidate used once
self.path.pop() # un-choose
What the code is doing, line by line:
- Sort the candidates, store them, and start with the whole target remaining.
- If
remaining == 0, save a copy ofpath. - In the loop, first skip duplicate siblings (
i > startand same value as the previous one), exactly like Subsets II. - Then stop the whole loop with
breakas soon as a number is bigger than what remains (the list is sorted). - Choose the number and recurse with
i + 1, because each candidate may be used at most once. Pop to undo.
Dry run on the example:
| Call (start, remaining) | path | Action | results |
|---|---|---|---|
| (0, 8) | [] | take 1 (index 0) | – |
| (1, 7) | [1] | take 1 (index 1) | – |
| (2, 6) | [1,1] | take 2 → (3, 4): 5 > 4 break | – |
| (2, 6) | [1,1] | take 5 → (4, 1): break; take 6 → (5, 0): save | [1,1,6] |
| (1, 7) | [1] | take 2 → (3, 5): take 5 → save | + [1,2,5] |
| (1, 7) | [1] | take 5, 6: dead ends; take 7 → save | + [1,7] |
| (0, 8) | [] | index 1 is another 1: skip | |
| (0, 8) | [] | take 2 → (3, 6): take 6 → save | + [2,6] |
| (0, 8) | [] | take 5, 6, 7: dead ends; 10 > 8 break | 4 combinations |
(candidates = [10,1,2,7,6,1,5] sorted to [1,1,2,5,6,7,10], target = 8.)
Edge cases to test:
- Many repeated candidates, e.g.
[1, 1, 1, 1]. - No combination exists.
- Candidates larger than the target.
Complexity: O(2ⁿ · n) time in the worst case, O(n) recursion depth.
Medium 22. Generate Parentheses Google Meta Amazon Microsoft
Problem: Return every well-formed string made of n pairs of parentheses.
Solve it on LeetCode ↗ · Video walkthrough ↗
n = 3["((()))","(()())","(())()","()(())","()()()"]n = 1["()"]Constraints: 1 ≤ n ≤ 8
Hint 1
At any point, you may add ( if you have some left, and ) only if it wouldn't close more than you've opened.
Hint 2
Backtrack with counts open and close: add ( while open < n, add ) while close < open.
From brute force to the pattern: Generating all 2²ⁿ strings and filtering the valid ones is wasteful. Respecting the two rules while building generates only valid strings (the Catalan number of them).
Spot it: "generate all" valid strings is backtracking where the validity rules prune as you go, so you never build an invalid string.
Approach (step by step):
- Track how many
'('(open_count) and')'(close_count) have been placed. - When the string has length
2n, record it. - You may add
'('whileopen_count < n. - You may add
')'only whileclose_count < open_count, meaning there's an unmatched'('to close. - Each addition is choose → recurse → pop.
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
self.n = n
self.path = [] # characters placed so far
self.results = []
self.backtrack(0, 0)
return self.results
def backtrack(self, open_count, close_count):
# open_count / close_count = how many '(' and ')' are already in path
if len(self.path) == 2 * self.n:
self.results.append("".join(self.path)) # n pairs placed: save the string
return
if open_count < self.n: # still have '(' left to place
self.path.append("(")
self.backtrack(open_count + 1, close_count)
self.path.pop()
if close_count < open_count: # a ')' must match an earlier unmatched '('
self.path.append(")")
self.backtrack(open_count, close_count + 1)
self.path.pop()
What the code is doing, line by line:
generateParenthesisstoresn, createspathandresults, and starts with no brackets placed.- When
pathhas2 * ncharacters, all pairs are placed; it is joined into a string and saved. - Choice 1: add
'(', allowed only while fewer thannhave been used. - Choice 2: add
')', allowed only while there is an unmatched'('(close_count < open_count). These two rules prune every invalid string before it is built. - Each choice is the usual append → recurse → pop.
Dry run on the example:
| path | open, close | Allowed next | Action |
|---|---|---|---|
"" | 0, 0 | ( only | add ( |
( | 1, 0 | ( or ) | add ( |
(( | 2, 0 | ( or ) | add ( |
((( | 3, 0 | ) only | add ) three times → save ((())) |
(( | 2, 0 | back here; now add ) | |
(() | 2, 1 | ( or ) | add ( → (()( → ) ) → save (()()) |
(() | 2, 1 | now add ) → (()) → ( ) → save (())() | |
( | 1, 0 | add ) → (): gives ()(()) and ()()() |
(n = 3: 5 strings.)
Edge cases to test:
n = 1→["()"].- Never letting
closeexceedopen. - Larger
n: the count grows like the Catalan numbers (5 forn = 3).
Complexity: Catalan number of results, O(4ⁿ / √n) time overall; O(n) recursion depth.
Medium 131. Palindrome Partitioning Google Meta Amazon Microsoft
Problem: Split a string into pieces so that every piece is a palindrome. Return every possible split.
s = "aab"[["a","a","b"],["aa","b"]]s = "a"[["a"]]Constraints: 1 ≤ s.length ≤ 16 · s contains only lowercase English letters.
Hint 1
Choose the first piece: some palindromic prefix. Then partition the rest the same way.
Hint 2
Backtrack over the end index of the next piece; precomputing which substrings are palindromes (a DP table) speeds up the checks.
From brute force to the pattern: Trying every cut pattern is 2ⁿ⁻¹ partitions and checking each is O(n). Backtracking only continues from palindromic pieces, and a palindrome table makes each check O(1).
Spot it: "return all possible partitions". The choice at each step is where to cut next. That's backtracking over cut positions.
Approach (step by step):
backtrack(start): the prefix beforestartis already cut into palindromes.- If
start == len(s), record a copy of the pieces. - For each
endfromstartto the last index, take the pieces[start:end+1]. - If the piece is a palindrome (checked with a two-pointer helper,
is_palindrome): append it, recurse withbacktrack(end + 1), pop it. Otherwise prune.
from typing import List
class Solution:
def partition(self, s: str) -> List[List[str]]:
self.s = s
self.path = [] # palindromic pieces cut so far
self.results = []
self.backtrack(0)
return self.results
def backtrack(self, start):
# everything before 'start' is already cut into palindromes
if start == len(self.s):
self.results.append(self.path[:]) # the whole string is cut up: save a COPY
return
for end in range(start, len(self.s)): # choice = where this piece ends
if self.is_palindrome(start, end): # only palindromic pieces may be chosen
piece = self.s[start:end + 1]
self.path.append(piece) # choose
self.backtrack(end + 1) # explore the rest of the string
self.path.pop() # un-choose
def is_palindrome(self, left, right):
# two pointers walk inwards; any mismatch means "not a palindrome"
while left < right:
if self.s[left] != self.s[right]:
return False
left = left + 1
right = right - 1
return True
What the code is doing, line by line:
partitionstores the string, sets uppath(pieces so far) andresults, and starts cutting at index 0.backtrack(start): ifstartreached the end, every character belongs to a piece, so a copy of the pieces is saved.- Otherwise try every possible end of the next piece.
is_palindrome(start, end)prunes pieces that are not palindromes. - For a good piece, slice it out with
s[start:end + 1](end is included), append it, recurse fromend + 1, then pop. is_palindromeuses two pointers moving towards each other; the first mismatched pair means "no", and if they meet without a mismatch the piece reads the same both ways.
Dry run on the example:
| Call | Piece tried | Palindrome? | path after choosing | results |
|---|---|---|---|---|
backtrack(0) | a | yes | [a] | – |
backtrack(1) | a | yes | [a,a] | – |
backtrack(2) | b | yes | [a,a,b] → start 3 = end: save | [a,a,b] |
backtrack(1) | ab | no: prune | [a] | |
backtrack(0) | aa | yes | [aa] | |
backtrack(2) | b | yes | [aa,b] → save | + [aa,b] |
backtrack(0) | aab | no: prune | [] | 2 partitions |
(s = "aab".)
Edge cases to test:
- A single character →
[["a"]]. - All the same letter (many partitions).
- No palindrome longer than 1 → just single letters.
Complexity: O(n · 2ⁿ) time (2ⁿ⁻¹ ways to cut, O(n) palindrome checks), O(n) recursion depth. Precompute a palindrome table to speed up the checks.
Hard 37. Sudoku Solver Google Amazon
Problem: Fill the empty cells ('.') of a 9×9 Sudoku so that every row, column and 3×3 box contains the digits 1–9 exactly once. Modify the board in place. A unique solution is guaranteed.
board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]][["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]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.
Hint 1
Find an empty cell and try digits 1–9 that don't break its row, column or box.
Hint 2
Keep sets for each row, column and box so validity checks are O(1); undo on failure. Filling the most constrained cell first is much faster.
From brute force to the pattern: Filling every empty cell with every digit and checking at the end is astronomically slow. Backtracking checks each placement immediately and abandons a branch as soon as a contradiction appears.
Spot it: This is constraint satisfaction: fill blanks one by one, prune clashing digits, and undo on dead ends. Because one solution is enough, the recursion returns True to stop everything as soon as it finds one.
Approach (step by step):
- Build sets of used digits for each row, column and box, and list the empty cells.
backtrack(empty_index): ifempty_index == len(empty_cells), every blank is filled, so return True.- For blank
(row, col)in box(row // 3) * 3 + col // 3, try digits 1–9 that aren't in the row, column or box sets. - Place the digit and add it to the three sets. If
backtrack(empty_index + 1)returns True, pass True straight up. - Otherwise remove the digit from the sets and the board (un-choose) and try the next one. If no digit fits, return False and the caller backtracks.
from typing import List
class Solution:
def solveSudoku(self, board: List[List[str]]) -> None:
self.board = board
# one set of digits already used per row, per column and per 3x3 box
self.row_digits = []
self.col_digits = []
self.box_digits = []
for i in range(9):
self.row_digits.append(set())
self.col_digits.append(set())
self.box_digits.append(set())
self.empty_cells = [] # every blank cell as [row, col]
for row in range(9):
for col in range(9):
digit = board[row][col]
if digit == ".":
self.empty_cells.append([row, col])
else:
box = self.box_index(row, col)
self.row_digits[row].add(digit)
self.col_digits[col].add(digit)
self.box_digits[box].add(digit)
self.backtrack(0)
def box_index(self, row, col):
# boxes are numbered 0..8, left to right, top to bottom
return (row // 3) * 3 + col // 3
def backtrack(self, empty_index):
# 'empty_index' = which blank cell we fill next
if empty_index == len(self.empty_cells):
return True # all blanks filled: solved
cell = self.empty_cells[empty_index]
row = cell[0]
col = cell[1]
box = self.box_index(row, col)
for digit in "123456789":
if digit in self.row_digits[row] or digit in self.col_digits[col]:
continue # clash in the row or column: prune
if digit in self.box_digits[box]:
continue # clash in the box: prune
# choose: write the digit and record it in all three sets
self.board[row][col] = digit
self.row_digits[row].add(digit)
self.col_digits[col].add(digit)
self.box_digits[box].add(digit)
if self.backtrack(empty_index + 1):
return True # stop at the first full solution
# un-choose: this digit led to a dead end
self.row_digits[row].remove(digit)
self.col_digits[col].remove(digit)
self.box_digits[box].remove(digit)
self.board[row][col] = "."
return False # no digit fits: the caller tries its next digit
What the code is doing, line by line:
solveSudokubuilds 9 empty sets for rows, 9 for columns and 9 for boxes with a plain loop, then scans the board once: blanks go intoempty_cells, given digits go into their row, column and box sets.box_indexturns a cell into its box number:row // 3is the box-row (0, 1 or 2),col // 3the box-column, andbox-row * 3 + box-columngives 0..8.backtrack(empty_index)fills one blank. If every blank is filled it returnsTrue.- For the current blank it tries the digits 1–9, skipping any digit already in the cell's row, column or box (pruning).
- A digit that fits is written to the board and added to the three sets. If the rest of the puzzle can then be solved,
Trueis passed straight up and the search stops. - If not, the digit is removed from the sets and the cell becomes
'.'again (un-choose). If no digit fits,Falsetells the previous blank to try its next digit.
Dry run on the example:
| Blank | Row has | Column has | Box has | Digits tried | Placed |
|---|---|---|---|---|---|
| (0,2) | 5 3 7 | 8 | 5 3 6 9 8 | 1 | 1 |
| (0,3) | 5 3 7 1 | 1 8 4 | 7 1 9 5 | 1 (row), 2 | 2 |
| (0,5) | 5 3 7 1 2 | 5 3 9 | 7 1 9 5 2 | 1, 2, 3 (row), 4 | 4 |
| … | deeper blanks keep going until one has no legal digit | ||||
| dead end | False travels back; placements are undone and the next digit is tried | ||||
| finish | first row ends as 5 3 4 6 7 8 9 1 2 |
(The first few steps on the LeetCode example; the full search is too long to list. Note that the early guess 1 at (0,2) is later undone and replaced by 4.)
Edge cases to test:
- Puzzles that need deep backtracking.
- A nearly complete board.
- Restoring the sets correctly when undoing a choice.
Complexity: Exponential in the worst case (≤ 9blanks), but the set-based pruning makes real puzzles fast. O(81) extra space.
Hard 212. Word Search II Google Meta Amazon Cisco
Problem: Given a letter grid and a list of words, return every word that can be traced through 4-adjacent cells without reusing a cell inside a word.
board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]["eat","oath"]board = [["a","b"],["c","d"]], words = ["abcb"][]Constraints: m == board.length · n == board[i].length · 1 ≤ m, n ≤ 12 · board[i][j] is a lowercase English letter. · 1 ≤ words.length ≤ 3 * 10⁴ · 1 ≤ words[i].length ≤ 10 · words[i] consists of lowercase English letters. · All the strings of words are unique.
Hint 1
Running Word Search once per word repeats the same grid exploration many times.
Hint 2
Put all the words in a trie and DFS the grid once, walking the trie in parallel; stop as soon as the path isn't a prefix of any word.
From brute force to the pattern: One backtracking search per word is O(words · r · c · 4^L). A trie lets one DFS check every word at once and prune any path that isn't a prefix; removing found words keeps it from repeating work.
Spot it: This is Word Search (79) for many words at once. Running 79 once per word is too slow. Put all words in a Trie and backtrack over the grid once, pruning the moment the current path isn't a prefix of any word.
Approach (step by step):
- Build a Trie (nested dicts). At the node where a word ends, store the word under the key
'$'. - From every cell whose letter starts some word, DFS while walking the Trie in lockstep.
- If the next cell's letter isn't a child of the current Trie node, don't go there (prune).
- When a node has
'$', record the word and pop it, so each word is reported once. - Mark the cell
'#'while it's on the path and restore it afterwards (choose / un-choose). - Optimization: when a Trie node has no children left, delete it from its parent so later searches skip dead branches.
from typing import List
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
# build the trie: nested dicts, one level per letter
self.root = {}
for word in words:
node = self.root
for letter in word:
if letter not in node:
node[letter] = {} # new branch for this letter
node = node[letter]
node["$"] = word # end-of-word marker stores the word itself
self.board = board
self.rows = len(board)
self.cols = len(board[0])
# the 4 neighbours as [row change, column change]: up, right, down, left
self.directions = [[-1, 0], [0, 1], [1, 0], [0, -1]]
self.results = []
for row in range(self.rows):
for col in range(self.cols):
if board[row][col] in self.root: # only start where some word starts
self.dfs(row, col, self.root)
return self.results
def dfs(self, row, col, parent_node):
letter = self.board[row][col]
node = parent_node[letter] # walk one step down the trie with this letter
if "$" in node:
self.results.append(node["$"]) # a whole word ends here
del node["$"] # remove the marker so it is reported once
self.board[row][col] = "#" # choose: this cell is on the current path
for direction in self.directions:
next_row = row + direction[0]
next_col = col + direction[1]
if next_row < 0 or next_row >= self.rows or next_col < 0 or next_col >= self.cols:
continue # off the board
if self.board[next_row][next_col] in node: # the trie prunes here
self.dfs(next_row, next_col, node)
self.board[row][col] = letter # un-choose: put the letter back
if len(node) == 0:
del parent_node[letter] # branch fully used up: remove it from the trie
What the code is doing, line by line:
- First build a trie out of nested dictionaries: each key is a letter leading to the next level. Where a word ends, the key
'$'stores the whole word. - Store the board and its size, list the four directions, and start a DFS from every cell whose letter begins some word (is a key of the root).
dfs(row, col, parent_node)steps fromparent_nodeto the child for this cell's letter. If that child has'$', a word is complete: save it and delete the marker so it can't be saved twice.- Choose: mark the cell
'#'. Explore: for each in-bounds neighbour, recurse only if its letter is a child of the current trie node. Any path that is not a prefix of some word is never walked. - Un-choose: restore the letter. Then, if the trie node has become empty (all its words found), delete it from its parent so later searches skip that dead branch.
Dry run on the example:
| Step | Cell | Trie path | What happens | results |
|---|---|---|---|---|
| 1 | (0,0) o | o | start: root has o; mark | – |
| 2 | (0,1) a | oa | right neighbour is a child: go | – |
| 3 | (1,1) t | oat | (0,2) a is not a child of oa; down to t | – |
| 4 | (2,1) h | oath | '$' found: save, delete marker; node now empty | oath |
| 5 | unwind | restore cells; empty nodes h, t, a, o are deleted from the trie | ||
| 6 | (1,0) e | e | no neighbour is a: return | |
| 7 | (1,3) e → (1,2) a → (1,1) t | eat | '$' found: save | + eat |
| 8 | (2,3) r | r | no neighbour is a: rain not on board | [oath, eat] |
(Board oaan / etae / ihkr / iflv, words [oath, pea, eat, rain]; only the meaningful steps are listed.)
Edge cases to test:
- The same word reachable by several paths (report it once).
- Words that are prefixes of other words.
- No words found →
[].
Complexity: O(R·C·4·3L−1) time in the worst case (L = longest word), but Trie pruning keeps it far lower in practice. O(total letters in words) space for the Trie.
Medium 77. Combinations Google Meta Amazon
Problem: Return all combinations of k numbers chosen from 1..n (order inside a combination doesn't matter).
Solve it on LeetCode ↗ · Video walkthrough ↗
n = 4, k = 2[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]n = 1, k = 1[[1]]Constraints: 1 ≤ n ≤ 20 · 1 ≤ k ≤ n
Hint 1
Choose numbers in increasing order to avoid duplicates like [2, 1].
Hint 2
Backtrack with a start value; record the path when it has k numbers.
From brute force to the pattern: Generating all orderings and de-duplicating is wasteful. Increasing order produces each combination once; pruning when not enough numbers remain skips dead branches. O(k · C(n, k)).
Spot it: "All combinations" → backtracking. Choosing numbers in increasing order (each choice starts after the previous one) produces every combination exactly once, never [2, 1] as well as [1, 2].
Approach:
backtrack(start)extends the currentpathwith numbers fromstart..n.- If
pathhasknumbers, record a copy and return. - Otherwise for each
xfromstartton: choosex(append), explorebacktrack(x + 1), un-choose (pop).
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result = []
path = []
def backtrack(start):
if len(path) == k:
result.append(path[:]) # a copy: path keeps changing
return
for x in range(start, n + 1):
path.append(x) # choose
backtrack(x + 1) # explore: only bigger numbers may follow
path.pop() # un-choose
backtrack(1)
return result
What the code is doing, line by line:
path: the combination being built;result: the finished ones.if len(path) == k:a full combination.path[:]stores a copy; storingpathitself would leave every entry pointing at the same list, which ends up empty.for x in range(start, n + 1):only numbers after the last one chosen, which is what prevents duplicates in a different order.path.append(x),backtrack(x + 1),path.pop(): choose, explore, un-choose. The pattern's chant.- Optional pruning: if fewer than
k − len(path)numbers remain instart..n, this branch can't finish. Stopping the loop atn − (k − len(path)) + 1skips it.
Dry run on the example:
n = 4, k = 2 (LeetCode Example 1). Returns [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]].
| first choice | second choices tried | recorded |
|---|---|---|
| 1 | 2, 3, 4 | [1, 2], [1, 3], [1, 4] |
| 2 | 3, 4 | [2, 3], [2, 4] |
| 3 | 4 | [3, 4] |
| 4 | (none left) | — |
Edge cases to test:
k = n→ one combination.k = 1→nsingle numbers.n = 1.
Complexity: O(k · C(n, k)) time (each of the C(n, k) answers is copied in O(k)), O(k) recursion space besides the output.
Medium 47. Permutations II Google Amazon
Problem: Return all distinct permutations of nums, which may contain duplicates.
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
Hint 1
Start from the normal permutations backtracking.
Hint 2
Sort first. When are two branches guaranteed to produce the same permutations?
From brute force to the pattern: Generating all n! orderings and putting them in a set does the duplicate work anyway; skipping duplicates prunes it.
Spot it: All arrangements → backtracking. Duplicates create identical branches; after sorting, equal values sit together, and allowing equal values only in left-to-right order removes every repeat.
Why not another pattern? Generating all n! permutations and deduplicating with a set wastes time on repeats.
Approach:
- Sort
nums; keep ausedflag per index. - At each depth, try each unused index
i, skipping it ifnums[i] == nums[i − 1]and indexi − 1is not used. - When the path has
nitems, record a copy.
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
nums = sorted(nums) # equal values become neighbours
used = [False] * len(nums)
result = []
path = []
def place():
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
continue # equal copies are used left to right only: no repeats
used[i] = True
path.append(nums[i])
place()
path.pop() # undo the choice
used[i] = False
place()
return result
What the code is doing, line by line:
- Sorting puts copies of the same value next to each other.
- Requiring copy
i − 1to be used before copyifixes one order among identical values, so each distinct permutation is built once. path[:]stores a snapshot, becausepathkeeps changing.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal values → one permutation.
- No duplicates (same as Permutations).
- Single element.
Complexity: O(n · n!) time in the worst case (all distinct), O(n) recursion space.
Medium 784. Letter Case Permutation Google
Problem: Return every string obtainable by changing the case of each letter in s independently.
s = "a1b2"["a1b2","a1B2","A1b2","A1B2"]s = "3z4"["3z4","3Z4"]Constraints: 1 ≤ s.length ≤ 12 · s consists of lowercase English letters, uppercase English letters, and digits.
Hint 1
Each letter has two choices, each digit one.
Hint 2
Build the answers character by character.
From brute force to the pattern: Checking all 2^n masks including digit positions creates duplicates; branching only on letters avoids them.
Spot it: Each letter is a binary choice → backtracking (or building level by level): 2^(letters) results.
Why not another pattern? Iterating over all 2^n bitmasks also works but must skip digits; the branch-per-letter form is simpler.
Approach:
- Start with
[""]. - For each character: if it's a letter, extend every partial string with both cases; otherwise extend with the character.
class Solution:
def letterCasePermutation(self, s: str) -> List[str]:
results = [""]
for ch in s:
if ch.isalpha():
results = [r + c for r in results for c in (ch.lower(), ch.upper())] # branch into two cases
else:
results = [r + ch for r in results] # digits don't branch
return results
What the code is doing, line by line:
- Each letter doubles the number of partial strings; digits just extend them.
- This is the backtracking tree built breadth-first, level by level.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Only digits → one string.
- Mixed case input.
- A single letter → two strings.
Complexity: O(n · 2^L) time and space (L = number of letters).
Medium 1291. Sequential Digits Google Meta Amazon
Problem: Return, sorted, all numbers in [low, high] whose digits each increase by exactly 1 (like 1234).
low = 100, high = 300[123,234]low = 1000, high = 13000[1234,2345,3456,4567,5678,6789,12345]Constraints: 10 ≤ low ≤ high ≤ 10^9
Hint 1
How many sequential-digit numbers exist at all?
Hint 2
They are all substrings of "123456789".
From brute force to the pattern: Scanning [low, high] is up to 10⁹ numbers; there are only 36 candidates.
Spot it: There are only 36 such numbers (lengths 2–9). Each is a window of "123456789", so enumerate by length then start — already sorted.
Why not another pattern? Checking every number in [low, high] is up to 10⁹ checks.
Approach:
- For each length from
len(str(low))tolen(str(high)): - For each start
0..9 − length:x = int("123456789"[start:start + length]). - Keep
xiflow ≤ x ≤ high.
class Solution:
def sequentialDigits(self, low: int, high: int) -> List[int]:
digits = "123456789"
result = []
for length in range(len(str(low)), len(str(high)) + 1):
for start in range(10 - length):
x = int(digits[start:start + length]) # a window of consecutive digits
if low <= x <= high:
result.append(x)
return result
What the code is doing, line by line:
- A sequential-digit number is fully determined by its first digit and its length.
- Shorter numbers are smaller, and for equal length a later start is larger, so the output is sorted.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No numbers in range →
[]. high= 10⁹ (the largest is 123456789).lowandhighof the same length.
Complexity: O(1): at most 36 candidates.
Medium 1415. The k-th Lexicographical String of All Happy Strings of Length n Google
Problem: Happy strings use a, b, c with no two equal neighbours. Return the k-th happy string of length n, or "".
n = 1, k = 3"c"n = 1, k = 4""Constraints: 1 ≤ n ≤ 10 · 1 ≤ k ≤ 100
Hint 1
How many happy strings of length n are there?
Hint 2
How many start with 'a'? Use that to skip whole groups.
From brute force to the pattern: Backtracking through the first k happy strings is O(k · n); block counting is O(n).
Spot it: The backtracking tree is regular: 3 choices for the first letter, then 2 each. Each first letter covers 2^(n−1) strings, each later letter 2^(remaining). Skip whole branches by integer division instead of listing them.
Why not another pattern? Generating all 3 · 2^(n−1) strings with backtracking works for n ≤ 10, but counting branches is O(n).
Approach:
- If
k > 3 · 2^(n−1), return"". Usek −= 1. - First letter:
"abc"[k // 2^(n−1)],k %= 2^(n−1). - Each next position: the two letters differing from the previous one, in order; pick
options[k // 2^(left)].
class Solution:
def getHappyString(self, n: int, k: int) -> str:
block = 1 << (n - 1) # strings under each choice of the first letter
if k > 3 * block:
return ""
k = k - 1
result = ["abc"[k // block]]
k = k % block
for left in range(n - 2, -1, -1): # letters still to place after this one
options = [c for c in "abc" if c != result[-1]] # two choices, alphabetical
size = 1 << left
result.append(options[k // size]) # which half k falls into
k = k % size
return "".join(result)
What the code is doing, line by line:
- There are
3 · 2^(n−1)happy strings of lengthn. - After the first letter, every position has exactly two options (anything but the previous letter).
- Each option covers
2^(positions left)strings, sok // sizepicks it andk % sizemoves inside it.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
klarger than the count →"".n = 1.k= the last string.
Complexity: O(n) time and space.
Hard 60. Permutation Sequence Google Meta Amazon
Problem: Return the k-th permutation (1-indexed, in lexicographic order) of 1..n.
n = 3, k = 3"213"n = 4, k = 9"2314"Constraints: 1 ≤ n ≤ 9 · 1 ≤ k ≤ n!
Hint 1
How many permutations start with 1? With 2?
Hint 2
Use (n − 1)! to decide the first digit, then repeat on the rest.
From brute force to the pattern: Listing permutations in order until the k-th is up to n! steps; block counting is O(n²).
Spot it: Backtracking would list k permutations. Instead, count the branches: with n digits left, each choice of the first digit covers (n − 1)! permutations, so (k − 1) // (n − 1)! tells which digit comes next. This is backtracking that skips whole subtrees.
Why not another pattern? Generating permutations in order until the k-th is O(k · n) with k up to n!.
Approach:
digits = [1..n],k −= 1(0-indexed).- For each position:
block = (remaining − 1)!;idx = k // block; takedigits.pop(idx);k %= block.
from math import factorial
class Solution:
def getPermutation(self, n: int, k: int) -> str:
digits = [str(d) for d in range(1, n + 1)]
k = k - 1 # 0-indexed
result = []
while digits:
block = factorial(len(digits) - 1) # permutations per choice of the next digit
idx = k // block # which block k falls into
result.append(digits.pop(idx))
k = k % block # position inside that block
return "".join(result)
What the code is doing, line by line:
- With
mdigits left, choosing the next digit splits the remaining permutations intomequal blocks of(m − 1)!. k // blockpicks the block (the digit),k % blockis the index inside it.- Popping keeps the unused digits sorted for the next step.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
k = 1→ digits in order.k = n!→ digits reversed.n = 1.
Complexity: O(n²) time (list pops), O(n) space.
Hard 679. 24 Game Google
Problem: Can the four cards be combined with + − × / and parentheses to make exactly 24 (real division)?
cards = [4,1,8,7]truecards = [1,2,1,2]falseConstraints: cards.length == 4 · 1 ≤ cards[i] ≤ 9
Hint 1
Any expression combines two numbers at a time.
Hint 2
Pick two numbers, replace them with their sum, difference, product or quotient, and recurse.
From brute force to the pattern: Writing out every bracketed expression string and evaluating it works but is fiddly; pair merging is the same search, cleaner.
Spot it: Every expression combines two values at a time. Backtracking: choose an ordered pair, replace it by each possible result, and recurse on the shorter list; with one value left, check it's 24 (with a small tolerance for division).
Why not another pattern? Enumerating expression strings with parentheses is messy and error-prone; combining pairs covers every bracket structure.
Approach:
- If one number is left, return whether it's within
1e-6of 24. - For each pair
i ≠ j: the rest stays; trya + b,a − b,a · b, anda / b(ifb ≠ 0). - Recurse with the rest plus the result; return true as soon as any branch works.
class Solution:
def judgePoint24(self, cards: List[int]) -> bool:
def solve(nums):
if len(nums) == 1:
return abs(nums[0] - 24) < 1e-6 # floats: allow tiny error
for i in range(len(nums)):
for j in range(len(nums)):
if i == j:
continue
rest = [nums[t] for t in range(len(nums)) if t != i and t != j]
a, b = nums[i], nums[j]
results = [a + b, a - b, a * b] # ordered pairs cover b - a and b / a too
if b != 0:
results.append(a / b)
for r in results:
if solve(rest + [r]): # combine two numbers into one
return True
return False
return solve([float(c) for c in cards])
What the code is doing, line by line:
- Any expression tree combines two sub-results at its root, so repeatedly merging two numbers explores every tree.
- Trying ordered pairs handles the non-commutative
−and/in both directions. - Division can produce fractions like 1/3, so the final comparison uses a tolerance.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Division by zero (skip).
- Fractions needed (e.g. 8 / (3 − 8/3)).
- No solution → false.
Complexity: Constant: at most 12 · 4 · 6 · 4 · 2 · 4 ≈ 9,000 leaves for four cards.
Hard 1240. Tiling a Rectangle with the Fewest Squares Google
Problem: Return the minimum number of integer-sided squares that exactly tile an n × m rectangle (n, m ≤ 13).
n = 2, m = 33n = 5, m = 85Constraints: 1 ≤ n, m ≤ 13
Hint 1
Splitting into two rectangles (DP) fails for some sizes like 11 × 13.
Hint 2
Backtrack: always cover the lowest-leftmost empty cell, trying the largest square first.
Hint 3
Prune any branch that already uses as many squares as the best answer.
From brute force to the pattern: Trying squares at every free cell in any order explodes; the canonical lowest-leftmost order plus pruning keeps it small.
Spot it: No greedy or simple DP is correct (11 × 13 needs 6, the DP gives 8). With tiny sizes, backtracking: track the filled height of every column (a skyline), always place a square at the lowest-leftmost gap, try large squares first, and prune when the count reaches the best found.
Why not another pattern? The usual split DP (cut the rectangle in two) misses tilings that aren't made of two rectangles, like the 11 × 13 case.
Approach:
heights[c]= filled height of columnc(rectanglentall,mwide).- Find the column with the lowest height (leftmost). If it's
n, all is filled: update the best. - The square's max size is limited by the run of equal-height columns and the remaining height. Try sizes from largest to 1.
- Raise those columns, recurse with count + 1, then lower them back.
class Solution:
def tilingRectangle(self, n: int, m: int) -> int:
heights = [0] * m # filled height of each column
self.best = n * m # all 1x1 squares
def fill(count):
if count >= self.best:
return # can't beat the best tiling found so far
low = min(heights)
if low == n:
self.best = count # everything is covered
return
start = heights.index(low) # leftmost lowest column
end = start
while end < m and heights[end] == low and end - start + 1 <= n - low:
end += 1 # widest square that fits here
for size in range(end - start, 0, -1): # big squares first
for c in range(start, start + size):
heights[c] += size
fill(count + 1)
for c in range(start, start + size):
heights[c] -= size # undo
fill(0)
return self.best
What the code is doing, line by line:
- Filling the lowest, leftmost gap first means every tiling is built in one canonical order.
- A square placed there can't be wider than the flat run of equal heights, nor taller than the space left.
- Large squares first find good tilings early, which makes the
count >= bestpruning very effective.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n == m→ 1.n = 1→msquares.- 11 × 13 (needs 6, the split DP says 8).
Complexity: Exponential in the worst case, but fast for n, m ≤ 13 thanks to the pruning.
Hard 1723. Find Minimum Time to Finish All Jobs Google
Problem: Assign each job to one of k workers to minimise the largest total working time (≤ 12 jobs).
jobs = [3,2,3], k = 33jobs = [1,2,4,7,8], k = 211Constraints: 1 ≤ k ≤ jobs.length ≤ 12 · 1 ≤ jobs[i] ≤ 10⁷
Hint 1
The input is tiny (12 jobs). Try every assignment, but prune.
Hint 2
Workers with the same current load are interchangeable.
Hint 3
Stop a branch as soon as some worker's load reaches the best answer found.
From brute force to the pattern: All kⁿ assignments is up to 12¹² without pruning. Symmetry breaking and bounding cut it to a small search.
Spot it: With 12 jobs, backtracking over assignments with strong pruning: sort jobs descending, stop any branch whose load reaches the best answer, and try only one of several workers with identical loads (they're interchangeable).
Why not another pattern? Greedy (give each job to the least loaded worker) isn't optimal; kⁿ assignments without pruning is too many.
Approach:
- Sort jobs descending;
loads = [0] * k;best = sum(jobs). - Place job
ion each worker whose load + job < best, skipping loads already tried at this level. - When all jobs are placed,
best = max(loads).
class Solution:
def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
jobs = sorted(jobs, reverse=True) # big jobs first: bad branches fail early
loads = [0] * k
self.best = sum(jobs)
def assign(i):
if i == len(jobs):
self.best = min(self.best, max(loads))
return
tried = set()
for w in range(k):
if loads[w] in tried or loads[w] + jobs[i] >= self.best:
continue # same load = same outcome; or can't beat best
tried.add(loads[w])
loads[w] += jobs[i]
assign(i + 1)
loads[w] -= jobs[i] # undo
assign(0)
return self.best
What the code is doing, line by line:
- Workers with equal loads lead to symmetric subtrees, so only one of them is tried.
- A branch whose new load would reach
bestcan't improve it. - Placing big jobs first raises loads quickly, so pruning starts early.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
kequals the number of jobs → the largest job.k = 1→ the sum.- Many equal jobs (symmetry).
Complexity: Worst case O(kⁿ), much less in practice with the pruning (n ≤ 12).
Hard 3348. Smallest Divisible Digit Product II Google Meta
Problem: Return the smallest number ≥ num with no zero digits whose digit product is divisible by t, or "-1". num can have 2·10⁵ digits.
num = "1234", t = 256"1488"num = "12355", t = 50"12355"Constraints: 2 ≤ num.length ≤ 2 * 10⁵ · num consists only of digits in the range ['0', '9']. · num does not contain leading zeros. · 1 ≤ t ≤ 10¹⁴
Hint 1
Which primes can a product of digits contain? When is the answer impossible?
Hint 2
Keep as long a prefix of num as possible and raise one digit; the rest is free.
Hint 3
How few digits are needed to cover a given number's 2, 3, 5, 7 factors? Arrange them to make the suffix smallest.
From brute force to the pattern: Testing num, num + 1, … is hopeless for 2·10⁵ digits. Scanning positions from the right with a greedy suffix is near-linear.
Spot it: Digits only supply the primes 2, 3, 5, 7, so if t has another prime factor the answer is -1. Otherwise, like "next greater number" problems, keep the longest possible prefix of num and raise one digit; the free suffix must cover what's left of t with the fewest digits (greedily divide by 9, 8, …, 2), placed after 1s in increasing order.
Why not another pattern? Incrementing the number and testing is hopeless with 2·10⁵ digits; a digit DP over the 4 prime exponents is possible but heavier.
Key insight: The fewest digits whose product is a multiple of r come from dividing r greedily by 9, 8, …, 2 (as in Minimum Factorization). Sorted ascending after leading 1s, they give the smallest suffix of a given length.
Approach:
- If
thas a prime factor other than 2, 3, 5, 7, return"-1". - If
numhas a 0, replace it and everything after with 1s (the smallest zero-free number above). need[i]= what's left oftafter the firstidigits. Ifneed[n] == 1, returnnum.- From the last position back: try digits
num[i] + 1 … 9; if the remainingtfits in then − i − 1free digits, build the answer. - If no position works, use a longer number:
1…1+ the minimal digits, lengthmax(n + 1, digits needed).
from math import gcd
class Solution:
def smallestNumber(self, num: str, t: int) -> str:
rest = t
for p in (2, 3, 5, 7):
while rest % p == 0:
rest //= p
if rest != 1:
return "-1" # t has a prime factor no digit can supply
def min_digits(r): # fewest digits whose product is a multiple of r, ascending
out = []
for d in range(9, 1, -1):
while r % d == 0:
out.append(str(d))
r //= d
return out[::-1]
digits = list(num)
if "0" in digits:
z = digits.index("0")
digits[z:] = ["1"] * (len(digits) - z) # smallest zero-free number above num
n = len(digits)
need = [t] # need[i]: part of t not yet covered by digits[:i]
for ch in digits:
need.append(need[-1] // gcd(need[-1], int(ch)))
if need[n] == 1:
return "".join(digits)
for i in range(n - 1, -1, -1): # rightmost position to raise = smallest result
for d in range(int(digits[i]) + 1, 10):
r = need[i] // gcd(need[i], d)
tail = min_digits(r)
free = n - i - 1
if len(tail) <= free:
return "".join(digits[:i]) + str(d) + "1" * (free - len(tail)) + "".join(tail)
tail = min_digits(t)
length = max(n + 1, len(tail)) # no same-length answer: go one digit longer
return "1" * (length - len(tail)) + "".join(tail)
What the code is doing, line by line:
- Any prime other than 2, 3, 5, 7 can never divide a product of digits 1–9.
- A zero must be raised; turning it into 1 (and everything after into 1s) is the smallest zero-free number ≥
num. need[i]shrinks as each prefix digit contributes its factors (gcdremoves what's covered).- Raising the rightmost possible digit keeps the longest prefix, giving the smallest result; the first
dthat works there is the smallest digit. - The free suffix is 1s then the minimal digits ascending; if nothing fits, the answer has more digits than
num.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
numalready works →num.numcontains zeros.twith a prime factor like 13 →"-1".- The answer is longer than
num.
Complexity: O(n · log t) time: the backwards scan stops within about 50 positions once the free suffix is long enough.
Medium 1239. Maximum Length of a Concatenated String with Unique Characters Meta
Problem: Choose a subsequence of arr whose concatenation has all distinct letters. Return the maximum length.
arr = ["un","iq","ue"]4arr = ["cha","r","act","ers"]6Constraints: 1 ≤ arr.length ≤ 16 · 1 ≤ arr[i].length ≤ 26 · arr[i] contains only lowercase English letters.
Hint 1
With at most 16 strings, you can try every subset.
Hint 2
Represent each string's letters as a bitmask to test conflicts quickly.
From brute force to the pattern: Building and checking the concatenated strings for each subset is slower; masks make checks O(1).
Spot it: Choose-or-skip each string → backtracking, at most 2¹⁶ branches. Represent a string's letters as a bitmask so a clash is mask & other != 0.
Why not another pattern? A DP over letter sets is possible but the plain search is small enough and clearer.
Approach:
- Turn each string into a mask; drop strings with repeated letters.
dfs(i, mask): either skiparr[i]or take it ifmask & m == 0.- Track the best
popcount(mask).
class Solution:
def maxLength(self, arr: List[str]) -> int:
masks = []
for word in arr:
m = 0
for ch in word:
bit = 1 << (ord(ch) - 97)
if m & bit:
m = -1 # a repeated letter: this word can never be used
break
m |= bit
if m >= 0:
masks.append(m)
self.best = 0
def dfs(i, used):
self.best = max(self.best, bin(used).count("1"))
for j in range(i, len(masks)):
if used & masks[j] == 0: # no shared letters
dfs(j + 1, used | masks[j]) # take word j (later words only)
dfs(0, 0)
return self.best
What the code is doing, line by line:
- A 26-bit integer records which letters are used; AND tests for overlap in
O(1). - Iterating
jfromibuilds each subset once (combinations, not permutations).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Strings with internal duplicates (skip).
- All 26 letters used.
- Single string.
Complexity: O(2ⁿ) time in the worst case (n ≤ 16), O(n) recursion depth.
Hard 301. Remove Invalid Parentheses Meta Amazon
Problem: Remove the minimum number of parentheses to make the string valid; return all distinct results.
s = "()())()"["(())()","()()()"]s = "(a)())()"["(a())()","(a)()()"]Constraints: 1 ≤ s.length ≤ 25 · s consists of lowercase English letters and parentheses '(' and ')'. · There will be at most 20 parentheses in s.
Hint 1
How many ( and how many ) must be removed at minimum?
Hint 2
Backtrack removing exactly that many, and skip equivalent removals.
From brute force to the pattern: Trying every subset of removals is O(2ⁿ · n); counting first and pruning duplicates cuts most of it.
Spot it: First compute exactly how many ( and ) must go (a counter scan). Then backtracking removes that many, keeping only valid results; skipping a character equal to the previous one at the same level avoids duplicate work.
Why not another pattern? BFS removing one character per level also works but generates many duplicates; the counted backtracking prunes harder.
Approach:
- Scan:
(→left += 1;)→left −= 1ifleft, elseright += 1. These are the removals needed. dfs(start, left, right, s): if both are 0 andsis valid, record it.- Try removing each bracket at
i ≥ start(skip ifs[i] == s[i − 1]fori > start).
class Solution:
def removeInvalidParentheses(self, s: str) -> List[str]:
left = right = 0 # how many "(" and ")" must be removed
for ch in s:
if ch == "(":
left += 1
elif ch == ")":
if left:
left -= 1
else:
right += 1
def valid(t):
bal = 0
for ch in t:
bal += (ch == "(") - (ch == ")")
if bal < 0:
return False
return bal == 0
found = set()
def dfs(start, left, right, t):
if left == 0 and right == 0:
if valid(t):
found.add(t)
return
for i in range(start, len(t)):
if i > start and t[i] == t[i - 1]:
continue # removing either of two equal neighbours gives the same string
if t[i] == ")" and right:
dfs(i, left, right - 1, t[:i] + t[i + 1:])
elif t[i] == "(" and left:
dfs(i, left - 1, right, t[:i] + t[i + 1:])
dfs(0, left, right, s)
return list(found)
What the code is doing, line by line:
- The first scan gives the minimum: unmatched
)while scanning, and(left open at the end. - Removing exactly that many keeps every answer minimal; the validity check filters wrong choices.
- Restarting the loop at
i(noti + 1) is correct because the string just got shorter at positioni.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already valid → [s].
- Only
)(→ [""]. - Letters mixed in.
Complexity: O(2ⁿ · n) in the worst case, much less with the counts and duplicate skipping.
Hard 489. Robot Room Cleaner Meta
Problem: Control a robot with move(), turnLeft(), turnRight() and clean() in an unknown room (no map, no position). Clean every reachable cell.
Solve it on LeetCode ↗ (Premium)
room = [[1,1,1,1,1,0,1,1],[1,1,1,1,1,0,1,1],[1,0,1,1,1,1,1,1],[0,0,0,1,0,0,0,0],[1,1,1,1,1,1,1,1]], row = 1, col = 3Robot cleaned all rooms.room = [[1]], row = 0, col = 0Robot cleaned all rooms.Constraints: m == room.length · n == room[i].length · 1 ≤ m ≤ 100 · 1 ≤ n ≤ 200 · room[i][j] is either 0 or 1 · 0 ≤ row < m · 0 ≤ col < n · room[row][col] == 1 · All the empty cells can be visited from the starting position
Hint 1
You can't see the map. Track visited cells in coordinates relative to the start.
Hint 2
DFS needs to undo moves physically: how do you get back to the previous cell facing the same way?
From brute force to the pattern: Random walking may never finish; DFS with physical backtracking visits each cell once.
Spot it: Exploring everything is DFS, but the robot can't jump back: undoing a step means physically going back — backtracking. Track visited cells in your own coordinates, starting from (0, 0) facing up.
Why not another pattern? BFS needs to teleport between frontier cells; the robot can only walk, so DFS with explicit backtracking fits.
Approach:
dfs(cell, dir): clean; for 4 directions (turning right each time): next cell = cell + dir.- If unvisited and
robot.move()succeeds:dfs(next, dir), then go back (turn twice, move, turn twice). - Turn right to try the next direction.
class Solution:
def cleanRoom(self, robot) -> None:
dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)] # up, right, down, left (clockwise)
visited = set()
def go_back():
robot.turnRight()
robot.turnRight()
robot.move() # step back to the previous cell
robot.turnRight()
robot.turnRight() # face the same way as before
def dfs(r, c, d):
visited.add((r, c))
robot.clean()
for k in range(4):
nd = (d + k) % 4
nr, nc = r + dirs[nd][0], c + dirs[nd][1]
if (nr, nc) not in visited and robot.move():
dfs(nr, nc, nd)
go_back() # undo the move (backtrack)
robot.turnRight() # try the next direction clockwise
dfs(0, 0, 0)
What the code is doing, line by line:
- Our coordinates are relative to the start; the robot's real position never matters.
- Four right turns bring the robot back to its original direction, so after the loop it faces the way it came in.
go_backreturns to the parent cell facing the same direction, exactly undoing the move.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single open cell.
- Long corridors.
- Obstacles forming loops.
Complexity: O(N) moves for N open cells (each edge walked twice), O(N) space.
Hard 980. Unique Paths III Meta
Problem: Count 4-directional walks from the start (1) to the end (2) that visit every non-obstacle cell exactly once.
grid = [[1,0,0,0],[0,0,0,0],[0,0,2,-1]]2grid = [[1,0,0,0],[0,0,0,0],[0,0,0,2]]4Constraints: m == grid.length · n == grid[i].length · 1 ≤ m, n ≤ 20 · 1 ≤ m * n ≤ 20 · -1 ≤ grid[i][j] ≤ 2 · There is exactly one starting cell and one ending cell.
Hint 1
The grid is tiny. Can you try every path?
Hint 2
Count the cells you must cover and check that count when you reach the end.
From brute force to the pattern: This is the exhaustive search; the cell count lets it reject incomplete paths at the end.
Spot it: Paths visiting each cell once on a tiny grid (≤ 20 cells) → backtracking: mark a cell, recurse, unmark.
Why not another pattern? DP over (cell, visited set) works with bitmasks but is overkill for 20 cells; plain backtracking is fast enough.
Approach:
- Count the cells to walk (
0s plus start); find the start. dfs(r, c, left): at the end, count 1 ifleft == 0.- Mark the cell as visited (−1), recurse into neighbours, unmark.
class Solution:
def uniquePathsIII(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
to_visit = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 1:
sr, sc = r, c
if grid[r][c] != -1:
to_visit += 1 # every walkable cell, including start and end
def dfs(r, c, left):
if grid[r][c] == 2:
return 1 if left == 1 else 0 # the end must be the last unvisited cell
saved, grid[r][c] = grid[r][c], -1 # mark as visited
paths = 0
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != -1:
paths += dfs(nr, nc, left - 1)
grid[r][c] = saved # unmark (backtrack)
return paths
return dfs(sr, sc, to_visit)
What the code is doing, line by line:
leftcounts cells not yet stepped on, including the current one.- Reaching the end with
left == 1means it's the final cell, so every cell was used. - Temporarily writing −1 doubles as the visited marker.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No valid path → 0.
- Start next to end with other cells left.
- Obstacles splitting the grid.
Complexity: O(3^k) time for k walkable cells (each step has at most 3 new directions), O(k) recursion.
Medium 93. Restore IP Addresses Nvidia Amazon Microsoft
Problem: Insert three dots into a digit string to form every valid IPv4 address (parts 0–255, no leading zeros).
s = "25525511135"["255.255.11.135","255.255.111.35"]s = "0000"["0.0.0.0"]Constraints: 1 ≤ s.length ≤ 20 · s consists of digits only.
Hint 1
Each part has 1, 2 or 3 digits. Try all choices for the first part, then recurse.
Hint 2
Reject parts with a leading zero or a value above 255, and stop when the rest can't fit.
From brute force to the pattern: Validating all C(n − 1, 3) dot placements is fine for n ≤ 20 but can't prune; backtracking stops invalid prefixes early.
Spot it: Choose where each of the 4 parts ends: a small decision tree (at most 3 choices per part) → backtracking. Prune a branch as soon as a part is invalid or the remaining digits can't fill the remaining parts.
Why not another pattern? Trying all C(n − 1, 3) dot placements and validating each works too, but backtracking stops bad prefixes early.
Approach:
place(start, parts): if 4 parts are chosen, record them whenstart == len(s).- Prune if the remaining digits are fewer than the parts left, or more than 3 per part.
- Try part lengths 1–3: skip leading zeros (
len > 1and starts with0) and values > 255.
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
result = []
parts = []
def place(start):
left = 4 - len(parts) # parts still to choose
if left == 0:
if start == len(s):
result.append(".".join(parts)) # used every digit
return
remaining = len(s) - start
if remaining < left or remaining > 3 * left:
return # can't split the rest into `left` parts of 1-3 digits
for length in range(1, 4):
piece = s[start:start + length]
if len(piece) < length:
break
if (length > 1 and piece[0] == "0") or int(piece) > 255:
continue # leading zero or out of range
parts.append(piece) # choose
place(start + length)
parts.pop() # un-choose
place(0)
return result
What the code is doing, line by line:
- Each level of the recursion picks the next part; there are exactly 4 levels.
- The length check prunes hopeless branches early (for example a 20-digit string fails immediately).
"0"is a valid part, but"01"is not.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
"0000"→0.0.0.0.- Strings longer than 12 → [].
- Zeros inside numbers (
101023).
Complexity: O(3⁴) branches at most, each costing O(n) to join: effectively constant.
Medium 89. Gray Code Amazon
Problem: Return an n-bit Gray code sequence: all 2ⁿ values starting at 0, consecutive values (and last-to-first) differ in exactly one bit.
n = 2[0,1,3,2]n = 1[0,1]Constraints: 1 ≤ n ≤ 16
Hint 1
Build the n-bit code from the (n − 1)-bit code.
Hint 2
Mirror the list and set the new top bit on the mirrored half.
From brute force to the pattern: Searching sequences with backtracking is exponential; reflection builds it directly.
Spot it: The recursive construction is a classic backtracking/reflection idea: the code for n bits is the code for n − 1 bits, followed by the same list reversed with the top bit set. Its closed form is i ^ (i >> 1).
Why not another pattern? Backtracking over sequences works for small n but is exponential to search.
Approach:
result = [0].- For each bit
b: appendx | (1 << b)forxinreversed(result).
class Solution:
def grayCode(self, n: int) -> List[int]:
result = [0]
for b in range(n):
result += [x | (1 << b) for x in reversed(result)] # mirror and set the new bit
return result
What the code is doing, line by line:
- Inside each half, neighbours differ by one bit (induction); at the seam, the mirrored copies differ only in the new bit.
- The last element is
1 << (n − 1), one bit from 0.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1 → [0, 1].
- Many valid sequences exist.
- Wrap-around condition.
Complexity: O(2ⁿ) time and space.
Medium 698. Partition to K Equal Sum Subsets Amazon Microsoft
Problem: Can nums be split into k non-empty subsets with equal sums?
nums = [4,3,2,3,5,2,1], k = 4truenums = [1,2,3,4], k = 3falseConstraints: 1 ≤ k ≤ nums.length ≤ 16 · 1 ≤ nums[i] ≤ 10⁴ · The frequency of each element is in the range [1, 4].
Hint 1
What must each subset sum to?
Hint 2
Backtrack assigning numbers to buckets; prune with sorting and duplicate loads.
From brute force to the pattern: Unpruned assignment is kⁿ; a bitmask DP over subsets is O(n · 2ⁿ) and also works.
Spot it: Assign each number to a bucket → backtracking, with pruning: sort descending, cap each bucket at target, and treat buckets with equal loads as the same.
Why not another pattern? Trying all kⁿ assignments without pruning is too slow.
Approach:
- If
sum % k, false;target = sum / k; if the largest > target, false. - Place numbers largest first; try each bucket with room, skipping duplicate loads.
- All placed → true.
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k:
return False
target = total // k
nums = sorted(nums, reverse=True) # big numbers first: fail fast
if nums[0] > target:
return False
buckets = [0] * k
def place(i):
if i == len(nums):
return True # every bucket must be exactly target
tried = set()
for b in range(k):
if buckets[b] + nums[i] <= target and buckets[b] not in tried:
tried.add(buckets[b]) # equal loads are interchangeable
buckets[b] += nums[i]
if place(i + 1):
return True
buckets[b] -= nums[i] # undo
return False
return place(0)
What the code is doing, line by line:
- All numbers placed with no bucket over
targetmeans every bucket is exactlytarget. - Skipping equal loads removes symmetric branches (including all the empty buckets).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Sum not divisible by k.
- A number bigger than target.
- k = 1 → true.
Complexity: O(kⁿ) worst case, far less with the pruning (n ≤ 16).
Medium 1980. Find Unique Binary String Amazon Microsoft
Problem: Given n distinct binary strings of length n, return any length-n binary string not among them.
nums = ["01","10"]"11"nums = ["00","01"]"11"Constraints: n == nums.length · 1 ≤ n ≤ 16 · nums[i].length == n · nums[i] is either '0' or '1'. · All the strings of nums are unique.
Hint 1
There are n strings and n positions.
Hint 2
Make your answer differ from the i-th string at position i.
From brute force to the pattern: Trying candidates against a set is O(n²) or so; the diagonal is O(n).
Spot it: Backtracking over strings would work, but Cantor's diagonal argument builds one directly: take the opposite of the i-th bit of the i-th string, so the result differs from every string somewhere.
Why not another pattern? Generating candidates and checking membership is fine for n ≤ 16 but the diagonal is O(n).
Approach:
- For each
i: output'1'ifnums[i][i] == '0'else'0'.
class Solution:
def findDifferentBinaryString(self, nums: List[str]) -> str:
return "".join("1" if nums[i][i] == "0" else "0" for i in range(len(nums))) # differ from string i at bit i
What the code is doing, line by line:
- The result differs from
nums[i]at positioni, so it equals none of them. - Any valid answer is accepted.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1.
- All strings share prefixes.
- Several valid answers.
Complexity: O(n) time, O(n) space.
Hard 140. Word Break II Amazon Microsoft
Problem: Return every way to split s into dictionary words, as space-separated sentences (any order).
s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]["cats and dog","cat sand dog"]s = "pineapplepenapple", wordDict = ["apple","pen","applepen","pine","pineapple"]["pine apple pen apple","pineapple pen apple","pine applepen apple"]Constraints: 1 ≤ s.length ≤ 20 · 1 ≤ wordDict.length ≤ 1000 · 1 ≤ wordDict[i].length ≤ 10 · s and wordDict[i] consist of only lowercase English letters. · All the strings of wordDict are unique. · Input is generated in a way that the length of the answer doesn't exceed 10⁵.
Hint 1
Try every dictionary word that matches a prefix and recurse on the rest.
Hint 2
The same suffix is solved many times: memoise it.
From brute force to the pattern: Plain backtracking repeats suffixes; memoisation reuses them.
Spot it: All sentences → backtracking over word prefixes; the same suffix is reached many ways, so memoise the sentences for each start index.
Why not another pattern? Word Break's boolean DP only says whether a split exists, not what they are.
Approach:
build(i): all sentences fors[i:];build(len(s)) = [""].- For each word
s[i:j]in the dictionary: prefix it to each sentence ofbuild(j).
from functools import lru_cache
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
words = set(wordDict)
@lru_cache(maxsize=None)
def build(i): # every sentence for s[i:]
if i == len(s):
return [""]
out = []
for j in range(i + 1, len(s) + 1):
word = s[i:j]
if word in words:
for rest in build(j):
out.append(word + (" " + rest if rest else "")) # word, then the rest
return out
return build(0)
What the code is doing, line by line:
[""]at the end lets the last word be added without a trailing space.- Memoisation shares suffix results between different prefixes.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No split → [].
- Words reused.
- Overlapping words (cat / cats).
Complexity: Proportional to the output size (can be exponential), O(n²) work per index otherwise.
Medium 216. Combination Sum III Microsoft
Problem: Find all combinations of k distinct digits 1–9 that sum to n.
k = 3, n = 7[[1,2,4]]k = 3, n = 9[[1,2,6],[1,3,5],[2,3,4]]Constraints: 2 ≤ k ≤ 9 · 1 ≤ n ≤ 60
Hint 1
Pick digits in increasing order.
Hint 2
Stop when the sum or the count is exceeded.
From brute force to the pattern: All 512 subsets would also work; backtracking is the standard approach.
Spot it: Choose a set of numbers → backtracking with increasing digits (no duplicates), pruning when the sum exceeds n.
Why not another pattern? Trying all subsets of 9 digits (512) works too; backtracking prunes earlier.
Approach:
pick(start, remaining): ifkdigits chosen, record whenremaining == 0.- Try digits
start..9whiled ≤ remaining.
class Solution:
def combinationSum3(self, k: int, n: int) -> List[List[int]]:
result = []
path = []
def pick(start, remaining):
if len(path) == k:
if remaining == 0:
result.append(path[:])
return
for d in range(start, 10):
if d > remaining:
break # larger digits only overshoot more
path.append(d)
pick(d + 1, remaining - d) # next digit must be larger
path.pop()
pick(1, n)
return result
What the code is doing, line by line:
- Increasing digits make each combination appear once.
- Digits are tried in increasing order, so the first one too big ends the loop.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No solution → [].
- k = 9.
- n too small.
Complexity: At most C(9, k) combinations; tiny.
Medium 526. Beautiful Arrangement Microsoft
Problem: Count permutations of 1..n where for every position i, perm[i] divides i or i divides perm[i].
n = 22n = 11Constraints: 1 ≤ n ≤ 15
Hint 1
Build the arrangement position by position.
Hint 2
Only try numbers that satisfy the rule for that position.
From brute force to the pattern: All permutations: n!; pruned backtracking (or bitmask DP O(n · 2ⁿ)).
Spot it: Place numbers position by position with pruning → backtracking (or a bitmask DP over used numbers).
Why not another pattern? Checking all n! permutations is too slow for n = 15.
Approach:
place(pos): ifpos > n, count one.- Try every unused
xwithx % pos == 0orpos % x == 0.
class Solution:
def countArrangement(self, n: int) -> int:
used = [False] * (n + 1)
def place(pos):
if pos > n:
return 1 # every position is filled
total = 0
for x in range(1, n + 1):
if not used[x] and (x % pos == 0 or pos % x == 0):
used[x] = True
total += place(pos + 1)
used[x] = False
return total
return place(1)
What the code is doing, line by line:
- The divisibility rule prunes most branches early.
- Filling from position 1 upward checks each placement once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- n = 1 → 1.
- n = 2 → 2.
- Larger n (pruning matters).
Complexity: Far below n! thanks to pruning; fine for n ≤ 15.