Skip to content
beginnerPhase 1 · Foundation

Backtracking

Master systematic exploration of all possibilities with choose-explore-unchoose pattern.

2h
5 problems
Topic Progress0%

What is Backtracking

Backtracking is a recursive technique for exploring all possible solutions by building a solution incrementally and abandoning ("backtracking") when a partial solution cannot lead to a valid complete solution.

Real-World Analogy

Imagine solving a maze. You walk forward until you hit a dead end. When you hit a dead end, you go back to the last intersection and try a different path. You keep doing this until you find the exit.

That's backtracking — try, if it fails, undo and try something else.

The Pattern

Every backtracking problem follows this template:

void backtrack(state, choices) {
    // Base case: valid solution found
    if (isSolution(state)) {
        result.add(new ArrayList<>(state));  // Must copy!
        return;
    }
    
    // Try each choice
    for (choice : choices) {
        // 1. Choose: add choice to state
        state.add(choice);
        
        // 2. Explore: recurse with updated state
        backtrack(state, remainingChoices);
        
        // 3. Unchoose: remove choice (backtrack)
        state.remove(state.size() - 1);
    }
}

Key Insight: Why Copy the State?

// WRONG: adding reference to state
result.add(state);  // state changes later!

// RIGHT: adding a copy
result.add(new ArrayList<>(state));  // independent copy

When to Use Backtracking

Signal Example
"Find ALL..." Find all subsets, all permutations
"Generate all..." Generate all valid parentheses
"Is there a..." Is there a valid arrangement?
Constraint satisfaction N-Queens, Sudoku
Explore and undo Maze solving, word search

Backtracking vs Other Patterns

Pattern Structure Output
Recursion Linear chain Single answer
Backtracking Tree exploration All valid solutions
DFS Graph traversal Visit all nodes
DP Subproblems Optimal answer

The Three Steps

  1. Choose: Make a decision (add to current state)
  2. Explore: Recursively explore consequences
  3. Unchoose: Undo the decision (remove from state)

This "choose-explore-unchoose" cycle is the essence of backtracking.

Backtracking Patterns

Pattern 1: Subsets

Generate all possible subsets of a set.

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, 0, new ArrayList<>(), result);
    return result;
}

private void backtrack(int[] nums, int start, 
                       List<Integer> state, List<List<Integer>> result) {
    result.add(new ArrayList<>(state));
    
    for (int i = start; i < nums.length; i++) {
        state.add(nums[i]);           // Choose
        backtrack(nums, i + 1, state, result);  // Explore
        state.remove(state.size() - 1);  // Unchoose
    }
}

Time: O(2^n) — each element is either included or not.

Pattern 2: Permutations

Generate all possible orderings.

public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(nums, new boolean[nums.length], new ArrayList<>(), result);
    return result;
}

private void backtrack(int[] nums, boolean[] used, 
                       List<Integer> state, List<List<Integer>> result) {
    if (state.size() == nums.length) {
        result.add(new ArrayList<>(state));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        
        used[i] = true;
        state.add(nums[i]);
        backtrack(nums, used, state, result);
        state.remove(state.size() - 1);
        used[i] = false;
    }
}

Time: O(n! × n) — n! permutations, each takes O(n) to copy.

Pattern 3: Combinations

Choose k items from n items.

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(n, k, 1, new ArrayList<>(), result);
    return result;
}

private void backtrack(int n, int k, int start, 
                       List<Integer> state, List<List<Integer>> result) {
    if (state.size() == k) {
        result.add(new ArrayList<>(state));
        return;
    }
    
    for (int i = start; i <= n; i++) {
        state.add(i);
        backtrack(n, k, i + 1, state, result);
        state.remove(state.size() - 1);
    }
}

Time: O(C(n,k) × k) — C(n,k) combinations.

Pattern 4: Constraint Satisfaction

N-Queens, Sudoku, etc.

// N-Queens template
private void backtrack(int row, int n, 
                       List<Integer> queens, List<List<String>> result) {
    if (row == n) {
        result.add(createBoard(queens, n));
        return;
    }
    
    for (int col = 0; col < n; col++) {
        if (isValid(queens, row, col)) {
            queens.add(col);        // Choose
            backtrack(row + 1, n, queens, result);  // Explore
            queens.remove(queens.size() - 1);  // Unchoose
        }
    }
}

Pattern Selection Guide

Pattern Key Question Example
Subsets Include or exclude each item? Subsets, Subsets II
Permutations What order? Permutations, Permutations II
Combinations Which k items? Combination Sum, Combinations
Constraint Valid arrangement? N-Queens, Sudoku, Word Search

Pruning the Search Space

Pruning means stopping early when you know a path cannot lead to a valid solution.

Why Prune?

Without pruning, backtracking explores ALL possibilities. With pruning, you skip impossible branches.

Example: Combination Sum

Find all combinations that sum to target.

// WITHOUT pruning (slower)
for (int i = start; i < candidates.length; i++) {
    state.add(candidates[i]);
    backtrack(candidates, target - candidates[i], i, state, result);
    state.remove(state.size() - 1);
}

// WITH pruning (faster)
Arrays.sort(candidates);  // Sort first!
for (int i = start; i < candidates.length; i++) {
    if (candidates[i] > target) break;  // PRUNE: no point continuing
    state.add(candidates[i]);
    backtrack(candidates, target - candidates[i], i, state, result);
    state.remove(state.size() - 1);
}

Pruning Techniques

  1. Sort first: Enables early termination
  2. Bound checking: If current state can't possibly lead to solution, stop
  3. Feasibility check: Before choosing, verify the choice is valid
  4. Symmetry breaking: Avoid exploring equivalent solutions

Example: N-Queens Pruning

private boolean isValid(List<Integer> queens, int row, int col) {
    for (int r = 0; r < row; r++) {
        int c = queens.get(r);
        if (c == col) return false;           // Same column
        if (Math.abs(r - row) == Math.abs(c - col)) return false;  // Diagonal
    }
    return true;
}

Only try columns that are valid — don't even recurse on invalid ones.

Complexity with Pruning

Problem Without Pruning With Pruning
N-Queens O(n^n) O(n!)
Combination Sum O(2^n) Much faster in practice
Sudoku O(9^81) O(1) in practice

Pruning doesn't change worst-case complexity, but dramatically improves average-case performance.

Java Implementation Patterns

Complete Template

public class BacktrackingTemplate {
    private List<List<Integer>> result = new ArrayList<>();
    
    public List<List<Integer>> solve(int[] nums) {
        backtrack(nums, 0, new ArrayList<>());
        return result;
    }
    
    private void backtrack(int[] nums, int start, List<Integer> state) {
        // Base case (if needed)
        if (/* valid solution */) {
            result.add(new ArrayList<>(state));  // COPY!
            return;
        }
        
        // Explore choices
        for (int i = start; i < nums.length; i++) {
            // Prune: skip invalid choices
            if (/* invalid */) continue;
            
            // Choose
            state.add(nums[i]);
            
            // Explore
            backtrack(nums, i + 1, state);
            
            // Unchoose
            state.remove(state.size() - 1);
        }
    }
}

Subsets Template

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    subsetsHelper(nums, 0, new ArrayList<>(), result);
    return result;
}

private void subsetsHelper(int[] nums, int start, 
                           List<Integer> current, List<List<Integer>> result) {
    result.add(new ArrayList<>(current));
    
    for (int i = start; i < nums.length; i++) {
        current.add(nums[i]);
        subsetsHelper(nums, i + 1, current, result);
        current.remove(current.size() - 1);
    }
}

Permutations Template

public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    permuteHelper(nums, new boolean[nums.length], new ArrayList<>(), result);
    return result;
}

private void permuteHelper(int[] nums, boolean[] used, 
                           List<Integer> current, List<List<Integer>> result) {
    if (current.size() == nums.length) {
        result.add(new ArrayList<>(current));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        
        used[i] = true;
        current.add(nums[i]);
        permuteHelper(nums, used, current, result);
        current.remove(current.size() - 1);
        used[i] = false;
    }
}

Word Search Template

public boolean exist(char[][] board, String word) {
    int m = board.length, n = board[0].length;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (dfs(board, word, i, j, 0)) return true;
        }
    }
    return false;
}

private boolean dfs(char[][] board, String word, int r, int c, int index) {
    if (index == word.length()) return true;
    if (r < 0 || r >= board.length || c < 0 || c >= board[0].length) return false;
    if (board[r][c] != word.charAt(index)) return false;
    
    char temp = board[r][c];
    board[r][c] = '#';  // Mark visited
    
    boolean found = dfs(board, word, r+1, c, index+1) ||
                    dfs(board, word, r-1, c, index+1) ||
                    dfs(board, word, r, c+1, index+1) ||
                    dfs(board, word, r, c-1, index+1);
    
    board[r][c] = temp;  // Unmark (backtrack)
    return found;
}

Practice Problems

0/5solved
Subsets
Backtracking - Subsets

Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets.

Example:

Input: nums = [1,2,3]

Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

All 2^3 = 8 subsets

Optimal Solution — O(n × 2^n) time, O(n) recursion depth space

Backtracking: for each element, choose to include or exclude

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, 0, new ArrayList<>(), result);
        return result;
    }
    
    private void backtrack(int[] nums, int start, List<Integer> state, List<List<Integer>> result) {
        result.add(new ArrayList<>(state));
        for (int i = start; i < nums.length; i++) {
            state.add(nums[i]);
            backtrack(nums, i + 1, state, result);
            state.remove(state.size() - 1);
        }
    }
}

Edge Cases:

  • Single element: [[], [1]]
  • All same elements: handle duplicates
  • Empty array: [[]]
Permutations
Backtracking - Permutations

Given an array nums of distinct integers, return all possible permutations. You can return the answer in any order.

Example:

Input: nums = [1,2,3]

Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

All 3! = 6 permutations

Optimal Solution — O(n! × n) time, O(n) recursion depth space

Backtracking with used[] array to track included elements

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(nums, new boolean[nums.length], new ArrayList<>(), result);
        return result;
    }
    
    private void backtrack(int[] nums, boolean[] used, List<Integer> state, List<List<Integer>> result) {
        if (state.size() == nums.length) {
            result.add(new ArrayList<>(state));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            state.add(nums[i]);
            backtrack(nums, used, state, result);
            state.remove(state.size() - 1);
            used[i] = false;
        }
    }
}

Edge Cases:

  • Single element: [[1]]
  • Two elements: [[1,2],[2,1]]
  • All same: handle duplicates with sorting
Combination Sum
Backtracking - Combinations

Given an array of distinct positive integers candidates and a target integer target, return all unique combinations where the chosen numbers sum to target. The same number may be chosen from candidates an unlimited number of times.

Example:

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

Output: [[2,2,3],[7]]

2+2+3 = 7 and 7 = 7

Optimal Solution — O(N^(T/M)) where N = candidates, T = target, M = min candidate time, O(T/M) recursion depth space

Backtracking with pruning: sort candidates, skip if candidate > remaining target

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, new ArrayList<>(), result);
        return result;
    }
    
    private void backtrack(int[] candidates, int remaining, int start,
                           List<Integer> state, List<List<Integer>> result) {
        if (remaining == 0) {
            result.add(new ArrayList<>(state));
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            if (candidates[i] > remaining) break;
            state.add(candidates[i]);
            backtrack(candidates, remaining - candidates[i], i, state, result);
            state.remove(state.size() - 1);
        }
    }
}

Edge Cases:

  • target = 0: return [[]]
  • No combination possible: return []
  • Single candidate: repeat it
N-Queens
Backtracking - Constraint Satisfaction

The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Return the number of distinct solutions.

Example:

Input: n = 4

Output: 2

Two distinct solutions exist for 4-queens

Optimal Solution — O(n!) time, O(n) recursion depth space

Backtracking: place queens row by row, check validity before placing

class Solution {
    public int totalNQueens(int n) {
        return backtrack(0, n, new ArrayList<>());
    }
    
    private int backtrack(int row, int n, List<Integer> queens) {
        if (row == n) return 1;
        int count = 0;
        for (int col = 0; col < n; col++) {
            if (isValid(queens, row, col)) {
                queens.add(col);
                count += backtrack(row + 1, n, queens);
                queens.remove(queens.size() - 1);
            }
        }
        return count;
    }
    
    private boolean isValid(List<Integer> queens, int row, int col) {
        for (int r = 0; r < row; r++) {
            int c = queens.get(r);
            if (c == col || Math.abs(r - row) == Math.abs(c - col)) return false;
        }
        return true;
    }
}

Edge Cases:

  • n = 1: 1 solution
  • n = 2: 0 solutions
  • n = 3: 0 solutions
Word Search
Backtracking - Grid Exploration

Given an m x n grid of characters board and a string word, return true if word exists in the grid. The word can be constructed from letters of sequentially adjacent cells (horizontally or vertically). The same cell may not be used more than once.

Example:

Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"

Output: true

Path: A→B→C→C→E→D

Optimal Solution — O(m × n × 4^L) where L = word length time, O(L) recursion depth space

DFS/Backtracking: try each cell as starting point, explore 4 directions

class Solution {
    public boolean exist(char[][] board, String word) {
        int m = board.length, n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, word, i, j, 0)) return true;
            }
        }
        return false;
    }
    
    private boolean dfs(char[][] board, String word, int r, int c, int index) {
        if (index == word.length()) return true;
        if (r < 0 || r >= board.length || c < 0 || c >= board[0].length) return false;
        if (board[r][c] != word.charAt(index)) return false;
        
        char temp = board[r][c];
        board[r][c] = '#';
        
        boolean found = dfs(board, word, r+1, c, index+1) ||
                        dfs(board, word, r-1, c, index+1) ||
                        dfs(board, word, r, c+1, index+1) ||
                        dfs(board, word, r, c-1, index+1);
        
        board[r][c] = temp;
        return found;
    }
}

Edge Cases:

  • Word longer than board: return false
  • Single cell board: check match
  • Word not found: return false

Quiz

1. What is the key difference between recursion and backtracking?

Question 1 options

2. In the subsets problem, why do we pass `i + 1` to the recursive call instead of `i`?

Question 2 options

3. What is the time complexity of generating all permutations of n elements?

Question 3 options

4. What is the primary purpose of Backtracking?

Question 4 options

Flashcards

Question

What are the 3 steps of backtracking?

Answer

1. Choose: add a choice to the current state. 2. Explore: recursively explore with the updated state. 3. Unchoose: remove the choice to try other options.

Question

When do you use backtracking?

Answer

When the problem asks to find ALL valid solutions, generate all possibilities, or check if any valid arrangement exists.

Question

Why must you copy the state when adding to results?

Answer

The state list is modified during backtracking. If you add a reference instead of a copy, all results will point to the same (final) state.

Question

What is Backtracking?

Answer

Backtracking is a key concept in software engineering.

Question

When to use Backtracking?

Answer

Use Backtracking when building production systems that require reliability, scalability, and maintainability.

Revision Notes

Key Takeaways

  • 1.Backtracking explores all possibilities by choosing, exploring, and unchoosing
  • 2.Always copy the state when adding to results (not a reference)
  • 3.Prune impossible branches to improve performance
  • 4.Subsets: O(2^n), Permutations: O(n!), Combinations: O(C(n,k))
  • 5.Identify the pattern: subsets, permutations, combinations, or constraint

Interview Tips

  • Clarify: do you need all solutions or just one?
  • Start with the template, then customize
  • Mention pruning to show optimization awareness
  • Trace through a small example first
  • State the time complexity using the pattern formula

Cheat Sheet

Backtracking Cheat Sheet

Template:

void backtrack(state, choices) {
    if (isSolution(state)) {
        result.add(copy(state));
        return;
    }
    for (choice : choices) {
        state.add(choice);    // Choose
        backtrack(state, ...); // Explore
        state.remove(choice); // Unchoose
    }
}

Patterns:

Pattern Key Example
Subsets Include/exclude Subsets, Subsets II
Permutations Order matters Permutations, Next Permutation
Combinations Choose k Combination Sum, Combinations
Constraint Valid arrangement N-Queens, Sudoku

Pruning:

  • Sort candidates first
  • Skip if candidate > remaining target
  • Check validity before recursing

Complexity:

  • Subsets: O(2^n)
  • Permutations: O(n! × n)
  • Combinations: O(C(n,k) × k)
  • N-Queens: O(n!)

Common Mistakes:

  1. Forgetting to copy state → all results are same
  2. Not unchoosing → wrong results
  3. No pruning → TLE on large inputs