12. Recursion & Backtracking
1508 words
8 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# 12. Recursion & Backtracking > **What problem does this solve?** Many problems are naturally defined in terms of smaller versions of themselves.

12. Recursion & Backtracking
What problem does this solve? Many problems are naturally defined in terms of smaller versions of themselves. Recursion lets us solve them elegantly. Backtracking extends recursion to problems where we must try multiple options, and "undo" choices that lead to dead ends.
1. Recursion Review
Mental Model
Recursion = a function that calls itself. Each call solves a smaller instance. When the instance is trivially small (base case), we stop.
The Three Laws of Recursion
- Base case: A condition that stops the recursion
- Smaller subproblem: Each call must be on a smaller input
- Self-call: The function must call itself
2. Classic Recursion Examples
Factorial
python# runnable def factorial(n): if n <= 1: # Base case return 1 return n * factorial(n - 1) # Smaller subproblem + self-call print(factorial(5)) # 120
Fibonacci (Exponential — Will Fix with DP)
python# runnable def fib(n): """Return nth Fibonacci. O(2^n) — terrible!""" if n <= 1: return n return fib(n - 1) + fib(n - 2) # Trace fib(5): # fib(5) = fib(4) + fib(3) # fib(4) = fib(3) + fib(2) # fib(3) = fib(2) + fib(1) = (fib(1)+fib(0)) + 1 = 2 # fib(2) = fib(1) + fib(0) = 1 # So: fib(5) = 5 print(fib(10)) # 55 # Note: fib(40) would take ~30 seconds!
Power Function
python# runnable def power(x, n): """Compute x^n. O(n).""" if n == 0: return 1 return x * power(x, n - 1) def power_fast(x, n): """Compute x^n using divide and conquer. O(log n).""" if n == 0: return 1 half = power_fast(x, n // 2) if n % 2 == 0: return half * half else: return x * half * half print(power(2, 10)) # 1024 print(power_fast(2, 10)) # 1024
3. Backtracking — The General Method
Mental Model
Backtracking is like exploring a maze. At each intersection (choice point), you try one path. If it leads to a dead end, you backtrack to the last intersection and try another path.
(Diagram)
Backtracking Template
python# runnable def backtrack(candidate, state): if is_solution(candidate): process_solution(candidate) return for next_candidate in generate_candidates(state): if is_valid(next_candidate, state): make_move(next_candidate, state) backtrack(next_candidate, state) undo_move(next_candidate, state) # Backtrack!
4. Backtracking Problem 1: N-Queens
Problem: Place N queens on an N×N chessboard such that no two queens attack each other.
How It Works
Place queens row by row. For each row, try each column. If the position is safe, place the queen and recurse to the next row. If no column works, backtrack.
python# runnable def solve_n_queens(n): """Return all solutions to N-Queens problem.""" solutions = [] def is_safe(board, row, col): """Check if placing queen at (row, col) is safe.""" # Check column for r in range(row): if board[r] == col: return False # Check diagonals if abs(board[r] - col) == abs(r - row): return False return True def backtrack(board, row): if row == n: # Found a solution — copy it solutions.append(board[:]) return for col in range(n): if is_safe(board, row, col): board[row] = col # Place queen backtrack(board, row + 1) # Recurse to next row board[row] = -1 # Backtrack (remove queen) backtrack([-1] * n, 0) return solutions def print_board(solution): """Pretty-print an N-Queens solution.""" n = len(solution) for col in solution: line = ['.'] * n line[col] = 'Q' print(' '.join(line)) print() solutions = solve_n_queens(4) print(f"Found {len(solutions)} solutions for 4-Queens:") for sol in solutions: print_board(sol) print() # Output for 4-Queens: # Solution 1: # . Q . . # . . . Q # Q . . . # . . Q .
Complexity
- Worst case: O(N! · N) — trying all permutations with safety checks
- Pruned heavily by the safety check — much faster in practice
- N = 8 has 92 solutions (explored in milliseconds)
5. Backtracking Problem 2: Sudoku Solver
Problem: Fill a 9×9 grid with digits 1-9 such that each row, column, and 3×3 box contains each digit once.
python# runnable def solve_sudoku(board): """Solve Sudoku in-place using backtracking. Returns True if solvable.""" def find_empty(): """Find next empty cell (0).""" for r in range(9): for c in range(9): if board[r][c] == 0: return r, c return None, None def is_valid(num, row, col): """Check if placing num at (row, col) is valid.""" # Check row for c in range(9): if board[row][c] == num: return False # Check column for r in range(9): if board[r][col] == num: return False # Check 3×3 box box_r, box_c = 3 * (row // 3), 3 * (col // 3) for r in range(box_r, box_r + 3): for c in range(box_c, box_c + 3): if board[r][c] == num: return False return True def backtrack(): row, col = find_empty() if row is None: # No empty cells → solved! return True for num in range(1, 10): if is_valid(num, row, col): board[row][col] = num if backtrack(): return True board[row][col] = 0 # Backtrack return False return backtrack() # Test board = [ [5, 3, 0, 0, 7, 0, 0, 0, 0], [6, 0, 0, 1, 9, 5, 0, 0, 0], [0, 9, 8, 0, 0, 0, 0, 6, 0], [8, 0, 0, 0, 6, 0, 0, 0, 3], [4, 0, 0, 8, 0, 3, 0, 0, 1], [7, 0, 0, 0, 2, 0, 0, 0, 6], [0, 6, 0, 0, 0, 0, 2, 8, 0], [0, 0, 0, 4, 1, 9, 0, 0, 5], [0, 0, 0, 0, 8, 0, 0, 7, 9] ] if solve_sudoku(board): for row in board: print(row) else: print("No solution exists")
6. Backtracking Problem 3: Generate All Subsets
python# runnable def generate_subsets(nums): """Generate all subsets (power set) using backtracking.""" result = [] def backtrack(start, current): result.append(current[:]) # Add current subset for i in range(start, len(nums)): current.append(nums[i]) # Include nums[i] backtrack(i + 1, current) # Recurse current.pop() # Backtrack backtrack(0, []) return result print(generate_subsets([1, 2, 3])) # [], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3](/courses/bscs2002/notes/%5D%2C%20%5B1%5D%2C%20%5B1%2C%202%5D%2C%20%5B1%2C%202%2C%203%5D%2C%20%5B1%2C%203%5D%2C%20%5B2%5D%2C%20%5B2%2C%203%5D%2C%20%5B3)
7. Backtracking Problem 4: Generate All Permutations
python# runnable def generate_permutations(nums): """Generate all permutations using backtracking.""" result = [] def backtrack(current, remaining): if not remaining: result.append(current[:]) return for i in range(len(remaining)): current.append(remaining[i]) backtrack(current, remaining[:i] + remaining[i+1:]) current.pop() backtrack([], nums) return result print(generate_permutations([1, 2, 3])) # [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1](/courses/bscs2002/notes/1%2C%202%2C%203%5D%2C%20%5B1%2C%203%2C%202%5D%2C%20%5B2%2C%201%2C%203%5D%2C%20%5B2%2C%203%2C%201%5D%2C%20%5B3%2C%201%2C%202%5D%2C%20%5B3%2C%202%2C%201)
8. Complexity of Backtracking
| Problem | Complexity | Pruning |
|---|---|---|
| N-Queens | O(N!) | Constraint propagation |
| Sudoku | O(9^(n²)) | Forward checking |
| Subsets | O(2ⁿ) | None (all subsets) |
| Permutations | O(n!) | None (all permutations) |
Practice Questions
Q1. Trace the factorial function for n=4. Show the call stack.
Q2. How many times is fib(2) called in the naive recursion for fib(5)?
Q3. Solve the 4-Queens problem manually. How many solutions exist?
Q4. Why does the naive Fibonacci have O(2ⁿ) complexity?
Q5. Modify the Sudoku solver to find ALL solutions.
Q6. Write a backtracking algorithm for the Knight's Tour problem.
Q7. What is the relationship between recursion and the stack data structure?
Q8. Write a recursive function to compute gcd(a, b) using Euclid's algorithm.
Q9. In N-Queens, why do we only track one queen per row?
Q10. Can backtracking solve optimization problems? Give an example.
AnswersA1.pseudofactorial(4) = 4 * factorial(3) factorial(3) = 3 * factorial(2) factorial(2) = 2 * factorial(1) factorial(1) = 1 ← base case factorial(2) = 2 * 1 = 2 factorial(3) = 3 * 2 = 6 factorial(4) = 4 * 6 = 24A2. fib(2) is called 3 times in fib(5) computation. This redundancy is what DP eliminates.A3. 2 solutions for 4-Queens.A4. Each call makes 2 more calls, forming a binary tree of depth n. Number of calls = O(2ⁿ).A5. Instead of returning True when solved, save the board state and continue (don't return, keep backtracking to find other solutions).A6. Starting from each square, try all valid knight moves recursively. Backtrack when all moves from current position are exhausted. Use Warnsdorff's heuristic for efficiency.A7. The call stack IS a stack! Each recursive call pushes a frame onto the call stack; returning pops the frame. The LIFO nature of the call stack matches recursion perfectly.A8.pythondef gcd(a, b): if b == 0: return a return gcd(b, a % b)A9. Since queens attack along rows, only one queen can exist per row. So we place exactly one queen per row, tracking only its column position. This reduces the search space from C(N², N) to N^N.A10. Yes. The Knight's Tour (visit all squares without repeating) is an optimization variant. The Graph Coloring problem and Hamiltonian Path are also classic backtracking optimization problems. Join Discord Previous11. Queues — FIFO Data StructureNext13. Dynamic Arrays & Amortized Analysis