← Back to Tutorial Index

Chapter 7: Backtracking

Master backtracking algorithms for permutations, combinations, and constraint satisfaction problems. Learn when and where to use backtracking.

1. Backtracking Fundamentals

📌 When and Where to Use

  • Constraint satisfaction: Problems with constraints (N-Queens, Sudoku)
  • Combinatorial problems: Permutations, combinations, subsets
  • Search problems: Finding all solutions or one solution
  • Real-world example: Scheduling with constraints, puzzle solving, generating all possible configurations, or resource allocation with rules

Backtracking Template

def backtrack(choices, path, result):
    """
    General backtracking template.
    
    Args:
        choices: Available choices at current step
        path: Current partial solution
        result: List to store all valid solutions
    """
    # Step 1: Base case - solution found
    if is_solution(path):
        result.append(path.copy())  # Save a copy
        return
    
    # Step 2: Try each choice
    for choice in choices:
        # Step 3: Check if choice is valid
        if is_valid(choice, path):
            # Step 4: Make choice (add to path)
            path.append(choice)
            
            # Step 5: Recurse with updated path
            backtrack(updated_choices, path, result)
            
            # Step 6: Backtrack (undo choice)
            path.pop()

Step-by-Step Explanation:

Step 1 - Base Case: If current path is a complete solution, save it.
Step 2 - Try Choices: Iterate through all possible choices.
Step 3 - Validate: Check if choice is valid given current path.
Step 4 - Make Choice: Add choice to path.
Step 5 - Recurse: Explore further with this choice.
Step 6 - Backtrack: Remove choice (undo) to try other possibilities.

2. Permutations

Example: All Permutations

def permute(nums):
    """
    Generate all permutations of array.
    
    Args:
        nums: List of distinct integers
    
    Returns:
        List of all permutations
    """
    result = []
    
    def backtrack(path, remaining):
        # Step 1: Base case - all elements used
        if len(remaining) == 0:
            result.append(path.copy())
            return
        
        # Step 2: Try each remaining element
        for i in range(len(remaining)):
            # Step 3: Make choice
            # Add current element to path
            path.append(remaining[i])
            
            # Step 4: Update remaining (remove chosen element)
            new_remaining = remaining[:i] + remaining[i + 1:]
            
            # Step 5: Recurse
            backtrack(path, new_remaining)
            
            # Step 6: Backtrack
            path.pop()
    
    # Step 7: Start backtracking
    backtrack([], nums)
    return result

Step-by-Step Code Walkthrough:

Step 1 - Base Case: If no elements remaining, we have a complete permutation.
Step 2 - Try Each: For each remaining element, try adding it to path.
Step 3 - Make Choice: Add element to current path.
Step 4 - Update Remaining: Remove chosen element from remaining list.
Step 5 - Recurse: Continue building permutation with updated path and remaining.
Step 6 - Backtrack: Remove element from path to try other choices.
Step 7 - Start: Begin with empty path and all elements as remaining.

Example:

nums = [1, 2, 3]

Result: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

3. Combinations

Example: All Combinations of Size K

def combine(n, k):
    """
    Generate all combinations of k numbers from 1 to n.
    
    Args:
        n: Range [1, n]
        k: Size of combination
    
    Returns:
        List of all combinations
    """
    result = []
    
    def backtrack(start, path):
        # Step 1: Base case - combination of size k found
        if len(path) == k:
            result.append(path.copy())
            return
        
        # Step 2: Try each number from start to n
        for i in range(start, n + 1):
            # Step 3: Make choice
            path.append(i)
            
            # Step 4: Recurse with next start (avoid duplicates)
            # Start from i+1 to ensure increasing order
            backtrack(i + 1, path)
            
            # Step 5: Backtrack
            path.pop()
    
    # Step 6: Start from 1
    backtrack(1, [])
    return result

Step-by-Step Code Walkthrough:

Step 1 - Base Case: If path has k elements, we have a valid combination.
Step 2 - Try Numbers: Try each number from start to n.
Step 3 - Make Choice: Add number to path.
Step 4 - Recurse: Continue with next start = i+1 (ensures increasing order, avoids duplicates).
Step 5 - Backtrack: Remove number to try other choices.
Step 6 - Start: Begin from number 1.

Example:

n = 4, k = 2

Result: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]

4. N-Queens Problem

📌 When and Where to Use

  • Constraint satisfaction: Placing items with constraints
  • Real-world example: Scheduling with conflicts, resource allocation with restrictions, or layout problems

Example: N-Queens Solution

def solve_n_queens(n):
    """
    Place n queens on n×n board so no two attack each other.
    
    Args:
        n: Size of board and number of queens
    
    Returns:
        List of all solutions (each solution is list of queen positions)
    """
    result = []
    
    def is_safe(row, col, board):
        # Step 1: Check column
        for i in range(row):
            if board[i] == col:
                return False
        
        # Step 2: Check diagonal (top-left to bottom-right)
        for i in range(row):
            if board[i] - i == col - row:
                return False
        
        # Step 3: Check anti-diagonal (top-right to bottom-left)
        for i in range(row):
            if board[i] + i == col + row:
                return False
        
        return True
    
    def backtrack(row, board):
        # Step 4: Base case - all queens placed
        if row == n:
            result.append(board.copy())
            return
        
        # Step 5: Try each column in current row
        for col in range(n):
            # Step 6: Check if safe to place queen
            if is_safe(row, col, board):
                # Step 7: Place queen
                board[row] = col
                
                # Step 8: Recurse to next row
                backtrack(row + 1, board)
                
                # Step 9: Backtrack (board[row] will be overwritten)
                # No explicit undo needed as we overwrite
    
    # Step 10: Start from row 0
    backtrack(0, [-1] * n)
    return result

Step-by-Step Code Walkthrough:

is_safe function:
  • Step 1: Check if any previous queen is in same column
  • Step 2: Check diagonal (same difference: row-col)
  • Step 3: Check anti-diagonal (same sum: row+col)
backtrack function:
  • Step 4: If all rows processed, save solution
  • Step 5: Try each column in current row
  • Step 6: Check if placing queen here is safe
  • Step 7: Place queen (store column in board[row])
  • Step 8: Recurse to next row
  • Step 9: Backtrack (will be overwritten in next iteration)
Step 10: Start from row 0 with empty board.

5. Subsets

Example: All Subsets

def subsets(nums):
    """
    Generate all subsets of array.
    
    Args:
        nums: List of integers
    
    Returns:
        List of all subsets
    """
    result = []
    
    def backtrack(start, path):
        # Step 1: Add current path (subset) to result
        # Add at every step, not just at base case
        result.append(path.copy())
        
        # Step 2: Try each element from start to end
        for i in range(start, len(nums)):
            # Step 3: Make choice
            path.append(nums[i])
            
            # Step 4: Recurse with next start
            backtrack(i + 1, path)
            
            # Step 5: Backtrack
            path.pop()
    
    # Step 6: Start from index 0
    backtrack(0, [])
    return result

Step-by-Step Code Walkthrough:

Step 1 - Add Subset: Add current path to result (every path is a valid subset).
Step 2 - Try Elements: Try each element from start position.
Step 3 - Make Choice: Include element in subset.
Step 4 - Recurse: Continue with next start (i+1) to avoid duplicates.
Step 5 - Backtrack: Remove element to try subsets without it.
Step 6 - Start: Begin from index 0.

Example:

nums = [1, 2, 3]

Result: [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

📚 Chapter Summary

  • Template: Make choice → Recurse → Backtrack (undo choice)
  • Permutations: Order matters, use all elements
  • Combinations: Order doesn't matter, use subset
  • Subsets: All possible subsets (power set)
  • Constraint Problems: Add validation (is_safe) before making choice

Key Takeaway: Backtracking explores all possibilities by trying choices, recursing, and undoing choices. Pruning (early termination) is crucial for efficiency.

← Previous: Chapter 6 Next: Chapter 8 - Greedy Algorithms →