Introduction to Backtracking
Backtracking is a systematic way to explore all possible solutions to a problem by making choices, exploring their consequences, and undoing (backtracking) when they don't lead to a solution.
The Core Idea
Think of backtracking like exploring a maze:
- Try a path
- If it leads to a dead end, go back
- Try a different path
- Repeat until you find the exit (or explore all paths)
Start → Choice 1 → Choice 2 → Choice 3 → Solution ✓
↓
Choice 2 → Dead end ✗ (backtrack)
↓
Choice 3 → Choice 4 → Solution ✓
When to Use Backtracking
Look for these keywords:
- "All possible combinations/permutations"
- "Generate all..."
- "Find all solutions"
- "Count all ways to..."
- Problems with choices and constraints
Problem Types
1. Generate All Solutions
- All permutations of array
- All subsets of set
- All combinations that sum to target
2. Decision Problems
- N-Queens placement
- Sudoku solver
- Word search in grid
3. Optimization with Exploration
- Path finding with constraints
- Resource allocation
- Scheduling problems
The Backtracking Process
def backtrack(state):
# 1. Base case: found solution
if is_solution(state):
save_solution(state)
return
# 2. Try all possible choices
for choice in get_choices(state):
# 3. Make choice
make_choice(choice, state)
# 4. Explore consequences
backtrack(state)
# 5. Undo choice (backtrack)
undo_choice(choice, state)
Simple Example: Generate Binary Strings
Problem: Generate all binary strings of length n.
def generate_binary(n):
result = []
current = []
def backtrack():
# Base case: string complete
if len(current) == n:
result.append(''.join(current))
return
# Try adding '0'
current.append('0')
backtrack()
current.pop() # Undo
# Try adding '1'
current.append('1')
backtrack()
current.pop() # Undo
backtrack()
return result
print(generate_binary(3))
# ['000', '001', '010', '011', '100', '101', '110', '111']
Trace for n=2:
Start: []
Add '0': ['0']
Add '0': ['0', '0'] → "00" ✓
Remove '0': ['0']
Add '1': ['0', '1'] → "01" ✓
Remove '1': ['0']
Remove '0': []
Add '1': ['1']
Add '0': ['1', '0'] → "10" ✓
Remove '0': ['1']
Add '1': ['1', '1'] → "11" ✓
Remove '1': ['1']
Remove '1': []
Key Characteristics
1. Recursive Structure
Backtracking uses recursion to explore possibilities.
2. State Management
You maintain and modify state as you explore.
3. Choice and Consequence
Make a choice, see where it leads, undo if necessary.
4. Pruning
Stop exploring paths that can't lead to solutions (optimization).
Backtracking vs. Other Approaches
vs. Brute Force
- Brute Force: Generate all possibilities, then check
- Backtracking: Build solutions incrementally, abandon bad paths early
vs. Dynamic Programming
- DP: Overlapping subproblems, optimal substructure
- Backtracking: Exploring choices, generating all solutions
vs. Greedy
- Greedy: Make best local choice
- Backtracking: Explore all choices systematically
Time Complexity
Backtracking is often exponential because it explores many possibilities:
- Permutations: O(n!)
- Subsets: O(2ⁿ)
- Combinations: O(2ⁿ) or O(C(n,k))
But pruning can significantly reduce the search space!
Example: Generate Permutations
def permutations(nums):
result = []
current = []
used = [False] * len(nums)
def backtrack():
# Base case
if len(current) == len(nums):
result.append(current[:]) # Make a copy!
return
# Try each number
for i in range(len(nums)):
if used[i]:
continue # Skip if already used
# Choose
current.append(nums[i])
used[i] = True
# Explore
backtrack()
# Unchoose
current.pop()
used[i] = False
backtrack()
return result
print(permutations([1, 2, 3]))
# [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]
Common Mistakes
❌ Forgetting to Backtrack
def wrong(nums):
result = []
current = []
def backtrack():
if len(current) == len(nums):
result.append(current) # Wrong! Will be modified
return
for num in nums:
current.append(num)
backtrack()
# Forgot to remove! ❌
backtrack()
return result
✅ Correct Version
def correct(nums):
result = []
current = []
def backtrack():
if len(current) == len(nums):
result.append(current[:]) # Copy!
return
for num in nums:
current.append(num)
backtrack()
current.pop() # Backtrack ✓
backtrack()
return result
Visualization
[]
/ \
[1] [2] [3]
/ \ / \ / \
[1,2][1,3][2,1][2,3][3,1][3,2]
| | | | | |
[1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1]
Each branch represents a choice. We explore depth-first and backtrack when done.
Key Takeaway
Backtracking is about systematically exploring possibilities. The key insight: make a choice, explore it fully, then undo it and try the next choice. This "choose-explore-unchoose" pattern is the heart of backtracking.