Quiz 2

12. Recursion & Backtracking

1508 words
8 min read
Python Week 1: the first filter for runtime behavior
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

  1. Base case: A condition that stops the recursion
  2. Smaller subproblem: Each call must be on a smaller input
  3. 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

ProblemComplexityPruning
N-QueensO(N!)Constraint propagation
SudokuO(9^(n²))Forward checking
SubsetsO(2ⁿ)None (all subsets)
PermutationsO(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.
Answers
A1.
pseudo
factorial(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 = 24
A2. 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.
python
def 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
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.