All topicsยทTopic 09 / 09

Backtracking - Complete Guide

Backtracking - Complete Guide

Introduction

Backtracking is a general algorithmic technique for solving problems recursively by trying to build a solution incrementally, one piece at a time, removing those solutions that fail to satisfy the constraints of the problem at any point of time. It is a systematic way to explore all possible configurations of a problem, often used for constraint satisfaction problems.

Key Characteristics

  • Recursive Exploration: It relies heavily on recursion to explore different paths.
  • State Restoration: It involves "undoing" changes (backtracking) when a path leads to a dead end.
  • Constraint Checking: It checks constraints at every step to prune invalid paths early.
  • Exponential Time: Typically has exponential time complexity as it explores all possibilities.

Core Techniques

1. The Backtracking Loop

A fundamental pattern where you iterate through candidates, make a choice, recurse, and then undo the choice.

Key Applications:

  • Solving Sudoku
  • Word Search puzzles

Learn more: The Backtracking Loop โ†’

2. Permutations

Generating all possible arrangements of a set of distinct elements.

Key Applications:

  • Next Permutation
  • Permutations II (with duplicates)

Learn more: Permutations โ†’

3. Combinations

Selecting a subset of items from a larger set where order does not matter.

Key Applications:

  • Combination Sum
  • Combination Sum III

Learn more: Combinations โ†’

4. Subsets

Finding the power set of a given array (all possible subsets).

Key Applications:

  • Subsets
  • Subsets II

Learn more: Subsets โ†’

Code Examples

// Example: Generating Subsets (Backtracking)
function subsets(nums: number[]): number[][] {
  const result: number[][] = [];
  
  function backtrack(start: number, current: number[]) {
    // Add a copy of the current subset to the result
    result.push([...current]);
    
    for (let i = start; i < nums.length; i++) {
      current.push(nums[i]); // Choose
      backtrack(i + 1, current); // Explore
      current.pop(); // Un-choose (Backtrack)
    }
  }
  
  backtrack(0, []);
  return result;
}
// Example: N-Queens (Constraint Satisfaction)
function solveNQueens(n: number): string[][] {
  const board: string[][] = Array.from({ length: n }, () => Array(n).fill('.'));
  const solutions: string[][] = [];
  
  function isValid(board: string[][], row: number, col: number): boolean {
    // Check column
    for (let i = 0; i < row; i++) {
      if (board[i][col] === 'Q') return false;
    }
    // Check upper left diagonal
    for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
      if (board[i][j] === 'Q') return false;
    }
    // Check upper right diagonal
    for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
      if (board[i][j] === 'Q') return false;
    }
    return true;
  }

  function backtrack(row: number) {
    if (row === n) {
      solutions.push(board.map(row => row.join('')));
      return;
    }
    
    for (let col = 0; col < n; col++) {
      if (isValid(board, row, col)) {
        board[row][col] = 'Q';
        backtrack(row + 1);
        board[row][col] = '.';
      }
    }
  }
  
  backtrack(0);
  return solutions;
}

Time and Space Complexity Guide

  • O(N!): For generating all permutations, the time complexity is factorial because there are N! ways to arrange N items.
  • O(2^N): For generating all subsets, the time complexity is exponential as there are 2^N subsets.
  • O(N): Space complexity is primarily determined by the recursion stack depth, which is O(N) in the worst case.

Learning Path

Beginner

  • Basic Recursion concepts
  • Subsets (Power Set)
  • Subsets II (Handling duplicates)

Intermediate

  • Permutations
  • Permutations II (Handling duplicates)
  • Combination Sum

Advanced

  • N-Queens
  • Word Search
  • Sudoku Solver
  • Palindrome Partitioning

Practice Strategy

  1. Master the State: Understand exactly what state you need to track (e.g., current index, current path, current sum).
  2. Visualize the Stack: Draw the recursion tree to understand how the algorithm explores and backtracks.
  3. Handle Duplicates: Practice identifying when to skip duplicates in loops to avoid redundant paths.
  4. Optimize Pruning: Learn to add early return conditions if the constraints are violated.

Common Mistakes to Avoid

  • Not Copying Arrays: Modifying the array in place and then backtracking without restoring the original state (or using a copy).
  • Missing Base Case: Forgetting to stop the recursion, leading to stack overflow or infinite loops.
  • Incorrect Loop Bounds: Using i++ instead of i + 1 when moving to the next recursive call, causing repeated use of the same element.
  • Ignoring Constraints: Not checking validity before recursing, leading to invalid states deep in the call stack.

Real-World Applications

  • Puzzle Solving: Solving logic puzzles like Sudoku and Crosswords.
  • Game AI: Determining all possible moves in Chess or Checkers.
  • Route Planning: Finding all possible paths in a graph or map.
  • Configuration Management: Generating valid configurations for hardware or software setups.

Summary

Backtracking is a powerful paradigm for solving constraint satisfaction problems where the solution space is vast but constrained. While it can be computationally expensive, understanding how to prune invalid paths and manage state is essential for any software engineer preparing for technical interviews.