The backtracking template
In one line
Backtracking builds a solution one choice at a time, explores each choice with recursion, and undoes it before trying the next, so it can search every valid answer without storing them all.
What it is
Backtracking is a way to search through all possible solutions by making decisions one at a time. At each step you pick one option, go deeper, and when that path is finished or proves useless you undo the choice and try the next option.
You can picture it as a depth-first walk over a tree of decisions. The root is the empty solution, each edge is one choice, and each leaf is a complete candidate. The key habit is choose, explore, un-choose: the shared state (a path list, a used set, a board) is modified before the recursive call and restored right after it.
Backtracking is the go-to technique when a problem says generate all, list every, find any arrangement, or count the ways, and the input is small (often n up to about 10 to 20).
Why interviewers ask
Subsets, permutations, combination sum, N-Queens, Sudoku and word search are all the same template with different choices and goal checks. Interviewers want to see that you can write it cleanly, keep the state correct when you undo, copy results at the right moment, and explain why the running time is exponential.
How it works
- 1Define the state: what partial solution you are building (for example a list called path) and any helpers (a used array, counters, sets of blocked cells).
- 2Define the goal check: when is the partial solution complete? If complete, record a copy of it (path[:]) and return.
- 3Define the choices available from this state, for example the numbers not used yet, or the two characters ( and ).
- 4For each choice that is still valid: apply it to the state (choose), recurse (explore), then reverse exactly what you applied (un-choose).
- 5Skip choices that can never lead to a valid answer. This is pruning, and it is what turns a brute force into a fast search.
- 6The running time is roughly the number of nodes in the decision tree times the work per node, which is why answers are usually written as O(2^n) or O(n!) times the cost of copying a result.
Worked example · Generate all valid parentheses for n = 2
- Start with path = [] and open = 0, close = 0. Rule: add ( if open < 2; add ) if close < open.
- Add ( : path = (, open 1. Add ( again: path = ((, open 2. Now only ) is allowed.
- Add ) : path = ((), close 1. Add ) : path = (()), close 2. Length is 4, so record "(())" and return.
- Undo back up to path = ( with open 1, close 0. The ( branch is done, so try ) : path = (), close 1.
- Add ( : path = ()(, open 2. Add ) : path = ()(), close 2. Record "()()".
- Undo all the way to the root. The ) branch at the root is not allowed (close would exceed open), so the search ends with ["(())", "()()"].
Picture it
It is like walking through a maze with a ball of string. At each junction you pick a corridor and unroll string behind you. When you hit a dead end you roll the string back to the last junction and try the next corridor. You never get lost, and you eventually try every route.
Reference code
def generate_parentheses(n):
result, path = [], []
def backtrack(open_count, close_count):
if len(path) == 2 * n: # goal: a full string
result.append("".join(path)) # record a copy
return
if open_count < n: # choice 1: add "("
path.append("(")
backtrack(open_count + 1, close_count)
path.pop() # undo
if close_count < open_count: # choice 2: add ")"
path.append(")")
backtrack(open_count, close_count + 1)
path.pop() # undo
backtrack(0, 0)
return result
print(generate_parentheses(2)) # ['(())', '()()']The path list is shared by every call. Each choice is appended before the recursive call and popped right after, so when control returns to a caller the path is exactly as it was. The two if checks prune invalid strings instead of generating all 2^(2n) strings and filtering. The number of results is the n-th Catalan number (5 for n = 3).
Key patterns
Choose, explore, un-choose
When: Any time state is shared across recursive calls
How: Mutate the shared list or set before the call and restore it immediately after, in reverse order.
Collect at leaves vs at every node
When: Permutations and fixed-length answers vs subsets
How: Record only when the path is complete for permutations; record at every call for subsets, because every node is a valid subset.
Find one vs find all
When: Sudoku or a yes/no question vs list every answer
How: For one answer, return True up the call stack as soon as the goal is reached and stop searching; for all answers, keep looping.
Count only
When: The problem asks how many ways
How: Return a count instead of building lists; if subproblems repeat, add memoization and it becomes dynamic programming.
Real-world use
Regular expression engines
Python's re module, Java's regex and PCRE match patterns by backtracking: when one alternative fails they back up and try another. Badly written patterns can backtrack exponentially, which caused a global Cloudflare outage in July 2019.
Dependency resolvers
pip's resolver (since version 20.3) tries package versions, and when it finds a conflict it backs out of the latest choice and tries an older compatible version.
SAT and constraint solvers
SAT solvers used in hardware verification and package managers are built on DPLL-style search: assign a variable, propagate its consequences, and backtrack on a conflict.
Prolog
The Prolog language runs every query by depth-first search with automatic backtracking over the rules that could match.
In a project
You are building a small exam timetable tool for a college: 12 exams, 5 time slots, and rules like no student sits two exams in the same slot.
- Model each exam as a decision: which slot does it get. The state is a map from exam to slot plus, for each slot, the set of students already sitting an exam in it.
- Recurse exam by exam. For each slot, check that no student of this exam is already busy in that slot; if valid, assign, recurse, then unassign.
- Order exams by most students first so conflicts show up early and bad branches die near the root.
- Stop at the first full assignment, or keep going and score each one if you want the best spread.
Impact: A manual timetable that took an administrator a day is produced in under a second, and it is guaranteed clash-free. The worst case is 5^12 (about 244 million) assignments, but pruning means only a tiny fraction is ever visited.
Trade-offs
Use it when
- The problem asks for all solutions, all arrangements or any valid arrangement
- Input is small (n up to roughly 10 to 20) and the answer itself is exponential in size
- Strong constraints let you abandon partial solutions early
Avoid it when
- Only a count or an optimum is needed and subproblems repeat: use dynamic programming
- A greedy rule or a formula gives the answer directly
- Input is large (n in the thousands): exponential search will not finish
Cost: Time is the number of nodes in the decision tree times the work per node, typically O(2^n * n) or O(n! * n). Extra space is O(n) for the recursion depth and path, plus the output.
Common mistakes
- Appending path itself instead of a copy (path[:]), so every stored result ends up as the same empty list.
- Forgetting to undo one of the changes (popping the path but not resetting used[i] or a set), which silently corrupts later branches.
- Checking validity only at the leaf instead of before recursing, which removes all the benefit of pruning.
- Not being able to state the complexity; saying 'it is recursive so O(n)' instead of counting the decision tree.
- Using global mutable state in an interview language like Python without nonlocal or a list wrapper, causing scoping bugs.
Interview questions
Quick revision
- Template: if goal: record copy, return; for each choice: if valid: choose, recurse, un-choose.
- Store path[:] (a copy), never path itself.
- Undo exactly what you did, in reverse order.
- Prune before recursing, not after.
- Complexity = nodes in the decision tree x work per node.
- Subsets O(n * 2^n), permutations O(n * n!).
- Need one answer: return True to stop early. Need a count with repeats: use DP.
Check yourself
1. What are the three steps inside the backtracking loop?
2. A backtracking solution stores results with result.append(path). What goes wrong?
3. How many valid strings does generate parentheses return for n = 3?
4. What is the extra space used by a backtracking search, not counting the output?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.