Backtracking

Medium
Time: O(2^n) subsets, O(n!) permutationsSpace: O(n) recursion depth
0/18
problems solved
0%
1.

Introduction to Backtracking

Backtracking is a systematic way to explore all possible solutions by building them incrementally and abandoning paths that cannot lead to valid solutions.

Think of it like exploring a maze:

  • You walk down a path until you hit a dead end
  • Then you "backtrack" to the last decision point
  • Try a different path
  • Repeat until you find the exit (or explore all paths)

Why is backtracking important?

About 15-20% of coding interview questions involve backtracking. It's essential for:

  • Generating all combinations/permutations
  • Solving puzzles (Sudoku, N-Queens)
  • Finding paths in graphs/grids
  • String partitioning problems

The Core Idea:

Backtracking = DFS + Pruning + Undo
1. Make a choice
2. Explore (recursively)
3. Undo the choice (backtrack)
4. Try next option

Time Complexity:

  • Subsets: O(2^n) - each element can be included or not
  • Permutations: O(n!) - n choices for first, n-1 for second, etc.
  • Generally exponential - but pruning helps!
2.

The Backtracking Template

Almost every backtracking problem follows this template:

function backtrack(currentState, choices):
if (goal reached):
add currentState to results
return
for each choice in choices:
if (choice is valid): // Pruning
make the choice // Modify state
backtrack(newState, ...) // Recurse
undo the choice // Backtrack <- CRITICAL!

The 3 Key Questions:

  1. What is the goal? (Base case)

    • Subsets: any state is valid
    • Permutations: path.length === n
    • Combination Sum: remaining === 0
  2. What are the choices?

    • Subsets: elements from index start to end
    • Permutations: all unused elements
    • Grid: 4 directions (up, down, left, right)
  3. When to prune?

    • Skip duplicates
    • Skip invalid states early
    • Check constraints before recursing
Code
Java
3.

Subsets - Generate All Subsets

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

Problem: Given [1, 2, 3], return all subsets.

Visualization:

[]
/ | \
[1] [2] [3]
/ \ |
[1,2] [1,3] [2,3]
|
[1,2,3]
Result: [], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]

Key Insight:

  • At each position, we have a choice: include element or not
  • Use start index to avoid duplicates ([1,2] and [2,1] are same subset)
  • Add current path to result at EVERY step (not just leaves)

Decision Tree:

For [1, 2, 3]:
start=0, path=[]
-> add [] to result
-> try 1: path=[1]
-> add [1] to result
-> try 2: path=[1,2]
-> add [1,2] to result
-> try 3: path=[1,2,3], add, backtrack
-> try 3: path=[1,3], add, backtrack
-> try 2: path=[2]
-> add [2] to result
-> try 3: path=[2,3], add, backtrack
-> try 3: path=[3], add, backtrack
Code
Java
4.

Interactive: Subsets Visualizer

Watch how backtracking builds all 2^n subsets by making include/exclude decisions at each element.

Subsets Generator

Generate all 2^n subsets using backtracking

Speed:
Input Array:
10
21
32
Current Path:
[]
Decision Tree:
                    []
           /        |        \
         [1]       [2]       [3]
        /   \       |
     [1,2] [1,3]  [2,3]
       |
    [1,2,3]
Results (0 / 8):
Click Play to generate all subsets

Key Insight: At each index, we have two choices: include the element or skip it. Total subsets = 2^n (each element can be in or out).

5.

Permutations - Generate All Orderings

Generate all possible orderings of elements.

Problem: Given [1, 2, 3], return all permutations.

Key Difference from Subsets:

  • Subsets: [1,2] and [2,1] are the SAME (order doesn't matter)
  • Permutations: [1,2] and [2,1] are DIFFERENT (order matters)

Visualization:

[]
/ | \
[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]

Key Insight:

  • Use a used array to track which elements are already in path
  • Start from index 0 every time (not from start)
  • Only add to result when path has all elements

Why used[] instead of start?

Subsets: start=1 means "don't go back to index 0"
This prevents [2,1] when we already have [1,2]
Permutations: We WANT [2,1] as a different result
So we check all indices, but skip used ones
Code
Java
6.

Interactive: Permutations Visualizer

Visualize how the used[] array tracks which elements are available as we build all n! orderings.

Permutations Generator

Generate all n! orderings using backtracking

Speed:
Available Elements:
1free
2free
3free
Current Permutation (building):
[ ]
Complete Permutations (0 / 6):
Click Play to generate all permutations

Key Difference from Subsets: We use a used[] array instead of a start index. This lets us pick any unused element at each position.

7.

Combinations - Choose K Elements

Choose exactly k elements from n elements.

Problem: Given n=4, k=2, return all combinations of 2 numbers from [1,2,3,4].

Visualization:

n=4, k=2
start=1, path=[]
-> try 1: path=[1]
-> try 2: path=[1,2] ✓ (length=k, add to result)
-> try 3: path=[1,3] ✓
-> try 4: path=[1,4] ✓
-> try 2: path=[2]
-> try 3: path=[2,3] ✓
-> try 4: path=[2,4] ✓
-> try 3: path=[3]
-> try 4: path=[3,4] ✓
-> try 4: path=[4]
-> (no more elements, can't reach k=2)
Result: [1,2], [1,3], [1,4], [2,3], [2,4], [3,4]

Pruning Optimization: If we need k elements and have path.length already, we need k - path.length more. So we only iterate while i <= n - (k - path.length) + 1.

Example: n=4, k=3, path=[1]
Need 2 more elements.
Can start from i=2, max i=3 (since we need 2 elements after i)
i=4 is useless - only 1 element left [4], can't make 2 more.
Code
Java
8.

Handling Duplicates

When input has duplicates, we need extra logic to avoid duplicate results.

Problem: [1, 2, 2] subsets should give [], [1], [2], [1,2], [2,2], [1,2,2] NOT: [], [1], [2], [2], [1,2], [1,2], [2,2], [1,2,2]

The Solution: Sort + Skip

Input: [1, 2, 2]
Sorted: [1, 2, 2] <- Always sort first!
At each level, skip element if:
nums[i] === nums[i-1] AND i > start
Why i > start?
- i === start: First occurrence at this level -> use it
- i > start: We already tried this value at this level -> skip

Visualization:

[1, 2, 2] sorted
start=0, path=[]
-> i=0: try 1, path=[1]
-> i=1: try 2, path=[1,2]
-> i=2: try 2, path=[1,2,2] ✓
-> i=2: SKIP (nums[2]=nums[1] and i>start)
-> i=1: try 2, path=[2]
-> i=2: try 2, path=[2,2] ✓
-> i=2: SKIP (nums[2]=nums[1] and i>start=0)
No duplicates!

For Permutations with Duplicates: Slightly different - skip if nums[i] === nums[i-1] && !used[i-1] This ensures we use duplicates in order.

Code
Java
9.

Combination Sum Problems

Find combinations that sum to a target.

Variations:

ProblemCan Reuse?Has Duplicates?Key Difference
Combination SumYesNoUse i (not i+1)
Combination Sum IINoYesSort + skip duplicates
Combination Sum IIINoNo (1-9)Fixed k elements

Combination Sum (can reuse elements):

candidates = [2, 3, 6, 7], target = 7
path=[], remaining=7
-> try 2: remaining=5
-> try 2: remaining=3
-> try 2: remaining=1
-> try 2: remaining=-1 ✗
-> try 3: remaining=-2 ✗
-> try 3: remaining=0 ✓ [2,2,3]
-> try 3: remaining=2
-> try 3: remaining=-1 ✗
-> try 3: remaining=4
-> try 3: remaining=1 ✗
-> try 6: remaining=1 ✗
-> try 7: remaining=0 ✓ [7]
Result: [[2,2,3], [7]]

Key Insight for Reuse:

  • backtrack(i, ...) allows reusing same element
  • backtrack(i + 1, ...) moves to next element
Code
Java
10.

Grid Backtracking - Word Search

Search for patterns in a 2D grid by exploring all paths.

Problem: Find if a word exists in a grid by moving up/down/left/right.

Visualization:

Grid: Word: "ABCCED"
A B C E
S F C S
A D E E
Path: A(0,0) -> B(0,1) -> C(0,2) -> C(1,2) -> E(2,2) -> D(2,1) ✓

Grid Backtracking Pattern:

  1. Try starting from each cell
  2. At each cell, try 4 directions
  3. Mark cell as visited before exploring
  4. Unmark after exploring (backtrack)

Key Insights:

  • Use the grid itself to mark visited (save space)
  • Replace char with '#', restore after backtracking
  • Check bounds and character match FIRST (pruning)

Common Grid Problems:

  • Word Search
  • N-Queens
  • Sudoku Solver
  • Rat in a Maze
  • Knight's Tour
Code
Java
11.

N-Queens - Constraint Satisfaction

Place N queens on an N×N board so no two attack each other.

Constraints:

  • No two queens in same row
  • No two queens in same column
  • No two queens on same diagonal

Visualization (N=4):

. Q . . Solution 1
. . . Q
Q . . .
. . Q .
. . Q . Solution 2
Q . . .
. . . Q
. Q . .

Smart Approach:

  • Place one queen per row (guarantees no row conflicts)
  • Track used columns with a Set
  • Track diagonals:
    • Main diagonal: row - col is constant
    • Anti-diagonal: row + col is constant

Diagonal Insight:

For a 4x4 board:
row-col (main diag): row+col (anti-diag):
0 -1 -2 -3 0 1 2 3
1 0 -1 -2 1 2 3 4
2 1 0 -1 2 3 4 5
3 2 1 0 3 4 5 6
Same value = same diagonal!
Code
Java
12.

Interactive: N-Queens Visualizer

Watch the backtracking algorithm place queens on a chessboard, detecting conflicts and trying alternatives.

N-Queens Solver

Place 4 queens on a 4x4 board with no conflicts

Speed:
Queen
Trying
Conflict
Solutions Found: 0
Click Play to solve 4-Queens

Constraint Checks: Same column (row-col), main diagonal (row-col constant), anti-diagonal (row+col constant).

13.

Generate Parentheses

Generate all valid combinations of n pairs of parentheses.

Constraints:

  • At any point, open count >= close count
  • Total open = Total close = n

Visualization (n=2):

""
/ \
"(" (invalid - can't start with ))
/ \
"((" "()"
/ \
"(()" "()("
| |
"(())" "()()"
Result: ["(())", "()()"]

Key Insight: We can add '(' if open < n, and ')' if close < open.

This is different from other backtracking because we don't iterate through choices - we make two specific decisions at each step.

Code
Java
14.

Palindrome Partitioning

Partition a string such that every substring is a palindrome.

Problem: "aab" -> [["a","a","b"], ["aa","b"]]

Visualization:

"aab"
start=0
-> s[0..0]="a" is palindrome
start=1
-> s[1..1]="a" is palindrome
start=2
-> s[2..2]="b" is palindrome
start=3 (end) -> ["a","a","b"] ✓
-> s[1..2]="ab" not palindrome, skip
-> s[0..1]="aa" is palindrome
start=2
-> s[2..2]="b" is palindrome
start=3 (end) -> ["aa","b"] ✓
-> s[0..2]="aab" not palindrome, skip
Result: [["a","a","b"], ["aa","b"]]

Key Insight: At each position, try all possible first partitions. Only continue if current partition is a palindrome.

Code
Java
15.

Common Edge Cases

Edge Cases to Handle:

1. Empty Input

if (nums.length === 0) return [[]];
if (s.length === 0) return [''];

2. Single Element

[5] subsets -> [[], [5]]
[5] permutations -> [[5]]

3. All Duplicates

[1,1,1] subsets -> [[], [1], [1,1], [1,1,1]]
// Must sort and skip duplicates properly

4. Large Input (Pruning Critical)

// Without pruning: TLE
// With pruning: AC
if (remaining < 0) return; // Don't continue
if (i > n - (k - path.length) + 1) break; // Not enough elements

5. No Solution Exists

combinationSum([2,4], 5) -> [] // Can't make odd with evens

Common Mistakes:

MistakeConsequence
Forget to copy pathAll results point to same array
Forget to backtrackState corrupted for other branches
Wrong loop boundsMissing or duplicate solutions
Not sorting for dupsDuplicate detection fails
Modify input in gridWorks if restored, else corrupted
16.

Comparison: Which Pattern to Use?

Quick reference for choosing the right backtracking approach:

By Problem Type:

ProblemPatternKey Identifier
Generate all subsetsSubsets"all subsets", "power set"
Generate all orderingsPermutations"all arrangements", "order matters"
Choose k from nCombinations"choose k", "select k"
Sum to targetCombination Sum"sum equals", "add up to"
Find path in gridGrid DFS2D array, "path", "search"
Place items with rulesN-Queens styleConstraints between items
Valid sequencesGenerate Parentheses"valid", "balanced"
Partition stringPalindrome Partition"partition", "all substrings"

Key Differences:

AspectSubsetsPermutationsCombinations
Loop startstart0start
Track usedNoYes (used[])No
Add to resultEvery callWhen completeWhen size = k
Next indexi + 10 (check used)i + 1

Handling Duplicates:

  • Always sort first
  • Skip: i > start && nums[i] == nums[i-1]
  • For permutations: also check !used[i-1]

Pruning Strategies:

  • Check constraints before recursing
  • For sums: skip if remaining < 0
  • For combinations: check if enough elements remain
  • For grids: check bounds and visited first
17.

Template Summary

Subsets Template:

function subsets(nums):
result = []
function backtrack(start, path):
result.add(copy(path)) // Add at every step
for i from start to nums.length:
path.add(nums[i])
backtrack(i + 1, path)
path.remove_last() // Backtrack
backtrack(0, [])
return result

Permutations Template:

function permutations(nums):
result = []
used = [false] * nums.length
function backtrack(path):
if path.length == nums.length:
result.add(copy(path))
return
for i from 0 to nums.length:
if used[i]: continue
used[i] = true
path.add(nums[i])
backtrack(path)
path.remove_last()
used[i] = false
backtrack([])
return result

Combination Sum Template:

function combinationSum(nums, target):
result = []
function backtrack(start, remaining, path):
if remaining == 0:
result.add(copy(path))
return
if remaining < 0: return
for i from start to nums.length:
path.add(nums[i])
// i (reuse) or i+1 (no reuse)
backtrack(i, remaining - nums[i], path)
path.remove_last()
backtrack(0, target, [])
return result

Grid Backtracking Template:

function gridBacktrack(grid, r, c, target):
if out_of_bounds or grid[r][c] != expected:
return false
if goal_reached:
return true
temp = grid[r][c]
grid[r][c] = '#' // Mark visited
found = gridBacktrack(r+1, c) or
gridBacktrack(r-1, c) or
gridBacktrack(r, c+1) or
gridBacktrack(r, c-1)
grid[r][c] = temp // Restore
return found

Interview Tips:

  • Draw the decision tree for small input
  • Identify: What's the goal? What are choices? When to prune?
  • Always copy path when adding to result
  • Remember to undo ALL state changes when backtracking

Test Your Knowledge

Take a quick 15question quiz to test what you've learned.