English Jackey

Backtracking Algorithm: Master the Art of Exploring All Possibilities

Complete guide to backtracking with interactive visualizations. Learn N-Queens, Permutations, Subsets, and Combination Sum with step-by-step animations and LeetCode solutions.

#algorithm #backtracking #recursion #dfs #visualization #leetcode #interview

Backtracking Algorithm: Master the Art of Exploring All Possibilities

Backtracking is a systematic method for exploring all possible solutions to a problem by building candidates incrementally and abandoning (“backtracking”) from partial solutions as soon as it’s determined they cannot lead to a valid solution. It’s the go-to algorithm for combinatorial problems in technical interviews.

Why Interviewers Love Backtracking

FAANG companies frequently ask backtracking problems because they test:

  1. Recursion mastery: Understanding call stacks and state management
  2. Problem decomposition: Breaking complex problems into smaller subproblems
  3. Optimization thinking: Pruning search spaces efficiently
  4. Edge case handling: Dealing with constraints and boundary conditions

Common patterns: Permutations, Combinations, Subsets, N-Queens, Sudoku Solver, Word Search

Visual Intuition: The Decision Tree

Backtracking explores a decision tree using Depth-First Search (DFS). Each node represents a state, and edges represent choices.

Permutations of [1,2,3]:

                    []
          /         |         \
        [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]
   
   6 leaf nodes = 3! = 6 permutations

Key Insight: We don’t need to build the entire tree upfront! Backtracking explores branches lazily and prunes invalid paths early.

Interactive Visualization: N-Queens

The N-Queens problem is the classic backtracking example. Place N queens on an N×N chessboard so no two queens attack each other.

Loading visualization...

Watch how the algorithm:

  1. Tries positions row by row
  2. Detects conflicts (same column/diagonal)
  3. Backtracks when stuck
  4. Finds the solution

The Universal Backtracking Template

Every backtracking problem follows this six-step pattern:

function backtrack(state):
    1. Base Case: if state is complete solution
         → add to results, return
    
    2. Iterate: for each possible choice
    
    3. Pruning: if choice is invalid
         → skip (continue)
    
    4. Choose: modify state (add choice)
    
    5. Recurse: explore next level
    
    6. Un-choose: restore state (backtrack)

Code Template

Loading...

Critical Point: Always deep copy when adding to results! (new ArrayList<>(path) in Java, path[:] in Python, [...path] in JS)


Problem 1: N-Queens (LC 51)

Problem Statement

Place n queens on an n×n chessboard such that no two queens attack each other (same row, column, or diagonal).

Approach

  • Choices: For each row, choose which column to place the queen
  • Constraints: No queens in same column, diagonal, or anti-diagonal
  • Optimization: Use sets to track blocked columns/diagonals in O(1)

Visualization

4×4 Board Solution:

  0 1 2 3
0 . Q . .    Queen at (0,1)
1 . . . Q    Queen at (1,3)
2 Q . . .    Queen at (2,0)
3 . . Q .    Queen at (3,2)

Blocked diagonals:
- Main diagonal (row - col): {-1, -2, 2, 1}
- Anti-diagonal (row + col): {1, 4, 2, 5}

Solution Code

Loading...

Detailed Complexity Analysis

Time Complexity: O(N!)O(N!) upper bound, practically O(N!N)O(N! \cdot N)

Let’s dive deep into why this complexity holds:

1. Decision Tree Size

Search space analysis:
Row 1: N choices
Row 2: ≤ N-2 choices (exclude: previous column + diagonal conflicts)
Row 3: ≤ N-4 choices
...

Worst case: N × (N-2) × (N-4) × ... ≈ O(N!)

2. More Precise Bounds

The actual complexity depends on implementation:

ImplementationTime ComplexityExplanation
Basic (loop validation)O(N!N)O(N! \cdot N)O(N) to check conflicts each placement
Set optimization (O(1) check)O(N!)O(N!)Track conflicts in sets, O(1) lookup
Bit manipulationO(2N)O(2^N)Use bitmasks, but larger constants

3. Why Not O(NN)O(N^N)?

Brute force without pruning: N^N placements
    ↓ One queen per row (row constraint)
Try N columns per row: N × N × N × ... = N^N
    ↓ Column conflict pruning
Each column used once: N × (N-1) × (N-2) × ... = N!
    ↓ Diagonal pruning
Actually much less than N! (≈ N!/e^N)

4. Empirical Measurements (solution count as lower bound)

NSolutionsN!Actual nodes explored
4224~16
89240,320~1,965
107243,628,800~13,576

Pruning is highly effective! Nodes explored << N!

5. Amortized Analysis

def count_operations(n):
    # Average valid choices at row i
    # Column pruning: n - i
    # Diagonal pruning: roughly halves remaining
    avg_choices_per_row = (n - i) * 0.5
    
    # Total operations approximation
    operations ≈ n × (n-1) × (n-2) × ... × 1 / 2^n
               = N! / 2^N
               ≈ O(√N × (N/e)^N)  (Stirling's approximation)

Space Complexity: O(N)O(N)

Detailed Breakdown:

1. Recursion Call Stack: O(N)
   - At most N levels deep (one per row)
   - Each stack frame contains: row, col variables
   
2. Board Storage: O(N²)
   - char[][] board = new char[n][n]
   - Can be optimized to O(N): store only column index per row
   
3. Auxiliary Data Structures (Set-optimized version): O(N)
   - Set<Integer> cols: O(N) - occupied columns
   - Set<Integer> diag1: O(N) - main diagonals (row-col)
   - Set<Integer> diag2: O(N) - anti-diagonals (row+col)
   
4. Result Storage: O(M × N²)
   - M = number of solutions (not counted in algorithm space)
   - Each solution needs N² space for board representation
   
Total (excluding results): O(N²) or O(N) when optimized

Space-Optimized Version:

// Optimization: use 1D array instead of 2D board
class Solution {
    List<List<String>> result = new ArrayList<>();
    int[] queens;  // queens[i] = j means queen at row i, column j
    Set<Integer> cols = new HashSet<>();
    Set<Integer> diag1 = new HashSet<>();
    Set<Integer> diag2 = new HashSet<>();
    
    public List<List<String>> solveNQueens(int n) {
        queens = new int[n];
        Arrays.fill(queens, -1);
        backtrack(0, n);
        return result;
    }
    
    private void backtrack(int row, int n) {
        if (row == n) {
            result.add(generateBoard(queens, n));
            return;
        }
        
        for (int col = 0; col < n; col++) {
            int d1 = row - col;
            int d2 = row + col;
            
            // O(1) conflict checking
            if (cols.contains(col) || 
                diag1.contains(d1) || 
                diag2.contains(d2)) {
                continue;
            }
            
            queens[row] = col;
            cols.add(col);
            diag1.add(d1);
            diag2.add(d2);
            
            backtrack(row + 1, n);
            
            // Backtrack
            queens[row] = -1;
            cols.remove(col);
            diag1.remove(d1);
            diag2.remove(d2);
        }
    }
}
// Space: O(N) - queens array + 3 sets
// Time: O(N!) - conflict check reduced to O(1)

Complexity Comparison Summary

ApproachTime ComplexitySpace ComplexityConflict CheckRating
2D array + loop checkO(N!N)O(N! \cdot N)O(N2)O(N^2)O(N)⭐⭐
2D array + SetO(N!)O(N!)O(N2)O(N^2)O(1)⭐⭐⭐⭐
1D array + SetO(N!)O(N!)O(N)O(N)O(1)⭐⭐⭐⭐⭐
Bit manipulationO(2N)O(2^N)O(N)O(N)O(1)⭐⭐⭐

Key Optimization Insights:

  1. Set-based tracking: Reduces O(N) validation to O(1)
  2. 1D array storage: Reduces O(N²) space to O(N)
  3. Early pruning: Drastically cuts branches explored
  4. Diagonal formulas: row-col and row+col identify diagonals efficiently

Key Interview Points

  1. Optimization: Track cols/diagonals in sets → O(1) validation instead of O(N)
  2. Diagonal formulas:
    • Main diagonal: row - col is constant
    • Anti-diagonal: row + col is constant
  3. Edge case: N = 1 returns [["Q"]]

Problem 2: Permutations (LC 46)

Problem Statement

Given an array of distinct integers nums, return all possible permutations.

Example: [1,2,3][[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Interactive Visualization

Loading visualization...

Approach

  • Choices: At each step, choose any unused number
  • Tracking: Use boolean[] used to mark chosen numbers
  • Base case: When path.size() == nums.length

Step-by-Step Example

nums = [1, 2, 3]

Step 1: Choose 1
  path = [1]
  used = [true, false, false]
  
  Step 2: Choose 2
    path = [1, 2]
    used = [true, true, false]
    
    Step 3: Choose 3
      path = [1, 2, 3] ← Complete! Add to result
      Backtrack...
      
    Step 3: No more choices
      Backtrack to Step 2...
      
  Step 2: Choose 3
    path = [1, 3]
    used = [true, false, true]
    ...

Solution Code

Loading...

Complexity Analysis

  • Time: O(NN!)O(N \cdot N!) - N! permutations, each takes O(N) to copy
  • Space: O(N)O(N) for recursion depth and used array

Common Mistakes

  1. Forgetting to deep copy: result.add(path) ❌ → result.add(new ArrayList<>(path))
  2. Not restoring state: Must set used[i] = false after recursion
  3. Duplicates handling: If input has duplicates (LC 47), need to sort and skip duplicates

Problem 3: Subsets (LC 78)

Problem Statement

Given an integer array nums of unique elements, return all possible subsets (the power set).

Example: [1,2,3][[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]

Key Insight

Unlike permutations, every state is a valid subset! We add to result at every recursion level.

Interactive Visualization

Loading visualization...

Approach

  • Choices: For each element, include or exclude it
  • No used array needed: Use start index to prevent revisiting
  • Base case: None! Add at every level

Binary Decision Tree

Subsets of [1, 2, 3]:

                      []
            /                    \
       include 1              don't include 1
          [1]                      []
       /      \                 /      \
   [1,2]      [1]           [2]        []
   /  \       /  \          /  \       /  \
[1,2,3][1,2] [1,3][1]   [2,3][2]   [3]  []

8 nodes = 2^3 = 8 subsets

Solution Code

Loading...

Complexity Analysis

  • Time: O(N2N)O(N \cdot 2^N) - 2N2^N subsets, each takes O(N) to copy
  • Space: O(N)O(N) for recursion depth

Interview Variations

  • LC 90: Subsets II (with duplicates) - Need to sort and skip duplicates
  • LC 131: Palindrome Partitioning - Similar structure but with validation

Problem 4: Combination Sum (LC 39)

Problem Statement

Given an array of distinct integers candidates and a target target, return all unique combinations where the chosen numbers sum to target. You may use the same number unlimited times.

Example:

Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3], [7]]

Approach

  • Key difference: Can reuse same element → pass i (not i+1)
  • Pruning: If sum > target, stop exploring
  • Optimization: Sort array and break early

Step-by-Step Trace

candidates = [2, 3, 6, 7], target = 7

Start: path = [], sum = 0

Try 2:
  path = [2], sum = 2
  Try 2 again: path = [2,2], sum = 4
    Try 2: path = [2,2,2], sum = 6
      Try 2: sum = 8 > 7 → Prune!
      Try 3: path = [2,2,3], sum = 7 ← Found!
    ...

Try 7:
  path = [7], sum = 7 ← Found!

Solution Code

Loading...

Complexity Analysis

  • Time: O(NT/M)O(N^{T/M}) where T = target, M = min(candidates)
  • Space: O(T/M)O(T/M) for recursion depth
  • Why this bound? Worst case: using smallest element repeatedly

Optimization Tricks

  1. Sort candidates: Enables early termination
  2. Pruning condition: if (sum + candidates[i] > target) break;
  3. Follow-up questions:
    • LC 40: Combination Sum II (each element used once)
    • LC 216: Combination Sum III (exactly k numbers)

Edge Cases & Common Pitfalls

1. Deep Copy Issue

// ❌ WRONG
result.add(path); // Adds reference, keeps changing!

// ✅ CORRECT
result.add(new ArrayList<>(path));

2. State Restoration

# ❌ WRONG
path.append(num)
backtrack()
# Forgot to remove!

# ✅ CORRECT
path.append(num)
backtrack()
path.pop()  # Must restore

3. Index Management

// Permutations: Can pick any unused element
for (let i = 0; i < nums.length; i++) { ... }

// Subsets/Combinations: Only pick elements ahead
for (let i = start; i < nums.length; i++) { ... }

Time & Space Complexity Summary

ProblemTimeSpaceExplanation
N-QueensO(N!)O(N!)O(N)O(N)N choices → (N-1) choices → …
PermutationsO(NN!)O(N \cdot N!)O(N)O(N)N! permutations × O(N) copy
SubsetsO(N2N)O(N \cdot 2^N)O(N)O(N)2N2^N subsets × O(N) copy
Combination SumO(NT/M)O(N^{T/M})O(T/M)O(T/M)Exponential in depth

General Formula: O(bd)O(b^d) where bb = branching factor, dd = depth

Interview Tips

When to Use Backtracking

Use backtracking when:

  • Problem asks for all solutions, not just one
  • Need to explore combinations/permutations
  • Constraints allow pruning the search space
  • Problem involves choices at each step

Don’t use when:

  • Only need one solution (try greedy/DP first)
  • Search space is too large without pruning
  • Problem has optimal substructure (DP is better)

How to Explain in Interview

  1. Draw the decision tree: Show first 2-3 levels
  2. Identify the pattern:
    • What are the choices at each step?
    • What’s the base case?
    • What constraints enable pruning?
  3. Start with brute force, then optimize
  4. Discuss complexity: Explain why it’s factorial/exponential

Advanced Optimizations

  1. Memoization: For overlapping subproblems (rare in backtracking)
  2. Bit manipulation: For subsets problems (alternative approach)
  3. Constraint propagation: For Sudoku-like problems
  4. Symmetry breaking: For N-Queens (only check half the board)

Practice Problems Roadmap

Easy

Medium

Hard


Key Takeaways

  1. Backtracking = DFS + State Restoration: Explore, then undo
  2. Six-step template works for 90% of problems
  3. Pruning is critical: Reduces exponential to manageable
  4. Deep copy when adding to results (most common bug!)
  5. Practice identifying the decision tree structure

Further Reading


Master these four problems (N-Queens, Permutations, Subsets, Combination Sum) and you’ll be ready for 95% of backtracking interview questions!