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.
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:
- Recursion mastery: Understanding call stacks and state management
- Problem decomposition: Breaking complex problems into smaller subproblems
- Optimization thinking: Pruning search spaces efficiently
- 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.
Watch how the algorithm:
- Tries positions row by row
- Detects conflicts (same column/diagonal)
- Backtracks when stuck
- 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
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
Detailed Complexity Analysis
Time Complexity: upper bound, practically
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:
| Implementation | Time Complexity | Explanation |
|---|---|---|
| Basic (loop validation) | O(N) to check conflicts each placement | |
| Set optimization (O(1) check) | Track conflicts in sets, O(1) lookup | |
| Bit manipulation | Use bitmasks, but larger constants |
3. Why Not ?
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)
| N | Solutions | N! | Actual nodes explored |
|---|---|---|---|
| 4 | 2 | 24 | ~16 |
| 8 | 92 | 40,320 | ~1,965 |
| 10 | 724 | 3,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:
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
| Approach | Time Complexity | Space Complexity | Conflict Check | Rating |
|---|---|---|---|---|
| 2D array + loop check | O(N) | ⭐⭐ | ||
| 2D array + Set | O(1) | ⭐⭐⭐⭐ | ||
| 1D array + Set | O(1) | ⭐⭐⭐⭐⭐ | ||
| Bit manipulation | O(1) | ⭐⭐⭐ |
Key Optimization Insights:
- Set-based tracking: Reduces O(N) validation to O(1)
- 1D array storage: Reduces O(N²) space to O(N)
- Early pruning: Drastically cuts branches explored
- Diagonal formulas: row-col and row+col identify diagonals efficiently
Key Interview Points
- Optimization: Track cols/diagonals in sets → O(1) validation instead of O(N)
- Diagonal formulas:
- Main diagonal:
row - colis constant - Anti-diagonal:
row + colis constant
- Main diagonal:
- 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
Approach
- Choices: At each step, choose any unused number
- Tracking: Use
boolean[] usedto 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
Complexity Analysis
- Time: - N! permutations, each takes O(N) to copy
- Space: for recursion depth and
usedarray
Common Mistakes
- Forgetting to deep copy:
result.add(path)❌ →result.add(new ArrayList<>(path))✅ - Not restoring state: Must set
used[i] = falseafter recursion - 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
Approach
- Choices: For each element, include or exclude it
- No used array needed: Use
startindex 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
Complexity Analysis
- Time: - subsets, each takes O(N) to copy
- Space: 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(noti+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
Complexity Analysis
- Time: where T = target, M = min(candidates)
- Space: for recursion depth
- Why this bound? Worst case: using smallest element repeatedly
Optimization Tricks
- Sort candidates: Enables early termination
- Pruning condition:
if (sum + candidates[i] > target) break; - 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
| Problem | Time | Space | Explanation |
|---|---|---|---|
| N-Queens | N choices → (N-1) choices → … | ||
| Permutations | N! permutations × O(N) copy | ||
| Subsets | subsets × O(N) copy | ||
| Combination Sum | Exponential in depth |
General Formula: where = branching factor, = 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
- Draw the decision tree: Show first 2-3 levels
- Identify the pattern:
- What are the choices at each step?
- What’s the base case?
- What constraints enable pruning?
- Start with brute force, then optimize
- Discuss complexity: Explain why it’s factorial/exponential
Advanced Optimizations
- Memoization: For overlapping subproblems (rare in backtracking)
- Bit manipulation: For subsets problems (alternative approach)
- Constraint propagation: For Sudoku-like problems
- Symmetry breaking: For N-Queens (only check half the board)
Practice Problems Roadmap
Easy
Medium
Hard
- LC 51: N-Queens
- LC 37: Sudoku Solver
- LC 212: Word Search II (Backtracking + Trie)
Key Takeaways
- Backtracking = DFS + State Restoration: Explore, then undo
- Six-step template works for 90% of problems
- Pruning is critical: Reduces exponential to manageable
- Deep copy when adding to results (most common bug!)
- 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!