Skip to content

Backtracking: Practice Problems with Full Solutions

Welcome to the practice problems for backtracking. If you need a refresher on the code, the code templates have the patterns in all six languages. Each problem below includes a hint, a visual walkthrough, and the full solution.

Recommended Study Order

The problems are ordered by difficulty, but the progression matters as much as the individual solutions.

  1. Letter Case Permutation introduces the choice-at-each-step pattern in its simplest form. Master this before adding complexity like tracking used elements or navigating grids.
  2. Subsets teaches the start index pattern that prevents duplicate permutations when order does not matter.
  3. Permutations adds the used array, which is necessary when every element must be used exactly once.
  4. Combinations layers a depth constraint on top of the subset pattern.
  5. Word Search extends backtracking to two dimensions with temporary state modification.
  6. Generate Parentheses demonstrates constraint-based pruning with multiple counters.
  7. Sudoku Solver combines multiple constraint checks into a single search on a 2D grid.
  8. N-Queens II introduces diagonal constraint tracking using sets or boolean arrays.
The order above is designed to build intuition progressively. The app schedules your reviews so you do not forget the Easy patterns while grinding Hard ones. Set up your review schedule .

Easy

1. Letter Case Permutation

LeetCode 784 | Difficulty: Easy

  • Brief: Given a string, return all possible strings you can create by transforming each letter individually to lowercase or uppercase.
  • Why this pattern?: Each character position offers a binary choice: leave digits as they are, or flip a letter between lowercase and uppercase. This is the simplest form of backtracking where every position has a fixed set of alternatives.
  • Key Insight: Digits are deterministic, so they just extend the string without branching. Letters create exactly two branches, which makes this a clean binary decision tree.

Visual:

    graph TD
    Root["'a1b'"] --> L["'a1b'"]
    Root --> U["'A1b'"]
    L --> LL["'a1b'"]
    L --> LU["'a1B'"]
    U --> UL["'A1b'"]
    U --> UU["'A1B'"]
  

Code:

var letterCasePermutation = function(s) {
    const result = [];

    function backtrack(i, current) {
        if (i === s.length) {
            result.push(current);
            return;
        }
        const char = s[i];
        if (/\d/.test(char)) {
            backtrack(i + 1, current + char);
        } else {
            backtrack(i + 1, current + char.toLowerCase());
            backtrack(i + 1, current + char.toUpperCase());
        }
    }

    backtrack(0, "");
    return result;
};

The decision tree has at most 2^L leaves where L is the number of letters, so the time complexity is

O(2^L)
. Each leaf corresponds to one completed string, and the recursion depth never exceeds the original string length, so the space is
O(N)
.


Medium

2. Permutations

LeetCode 46 | Difficulty: Medium

  • Brief: Given an array of distinct integers, return all possible permutations in any order.
  • Why this pattern?: You need to explore every possible ordering. Since you must use every element exactly once, this is a classic ordering backtracking problem.
  • Key Insight: Use a used array or hash set to keep track of elements already in your current path so you do not pick the same number twice.

Visual:

    graph TD
    Root["[]"] --> A["[1]"]
    Root --> B["[2]"]
    Root --> C["[3]"]
    A --> A1["[1,2]"]
    A --> A2["[1,3]"]
    A1 --> A11["[1,2,3]"]
    A2 --> A21["[1,3,2]"]
    style Root fill:#f9f,stroke:#333,stroke-width:2px
  

Code:

function permute(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);

    function backtrack(current) {
        if (current.length === nums.length) {
            result.push([...current]);
            return;
        }
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            current.push(nums[i]);
            backtrack(current);
            current.pop();
            used[i] = false;
        }
    }
    backtrack([]);
    return result;
}

3. Subsets

LeetCode 78 | Difficulty: Medium

  • Brief: Given an array of unique elements, return all possible subsets (the power set).
  • Why this pattern?: Every element has two choices: either it is included in the current subset or it is excluded.
  • Key Insight: To avoid duplicate sets when order does not matter, always iterate from a start index and only look forward.

Visual:

    graph TD
    Root["[]"] --> A["[1]"]
    Root --> B["[2]"]
    Root --> C["[3]"]
    A --> AB["[1,2]"]
    A --> AC["[1,3]"]
    AB --> ABC["[1,2,3]"]
    B --> BC["[2,3]"]
  

Code:

function subsets(nums) {
    const result = [];
    function backtrack(start, current) {
        result.push([...current]);
        for (let i = start; i < nums.length; i++) {
            current.push(nums[i]);
            backtrack(i + 1, current);
            current.pop();
        }
    }
    backtrack(0, []);
    return result;
}

4. Combinations

LeetCode 77 | Difficulty: Medium

  • Brief: Given two integers n and k, return all possible combinations of k numbers out of 1 ... n.
  • Why this pattern?: This is a direct subset problem with a depth constraint that stops at length k.
  • Key Insight: You can prune branches earlier if there are not enough remaining numbers to reach length k.

Visual:

    graph TD
    Root["[]"] --> A["[1]"]
    Root --> B["[2]"]
    Root --> C["[3]"]
    A --> AB["[1,2]"]
    A --> AC["[1,3]"]
    B --> BC["[2,3]"]
    style AB fill:#f96,stroke:#333,stroke-width:2px
    style AC fill:#f96,stroke:#333,stroke-width:2px
    style BC fill:#f96,stroke:#333,stroke-width:2px
  

Code:

function combine(n, k) {
    const result = [];
    function backtrack(start, current) {
        if (current.length === k) {
            result.push([...current]);
            return;
        }
        for (let i = start; i <= n; i++) {
            current.push(i);
            backtrack(i + 1, current);
            current.pop();
        }
    }
    backtrack(1, []);
    return result;
}

5. Word Search

LeetCode 79 | Difficulty: Medium

  • Brief: Given an m x n grid of characters and a word, return true if the word exists in the grid.
  • Why this pattern?: You are exploring a 2D plane. Since you cannot reuse the same cell in a single word path, you must mark cells as visited for the duration of the current search and unmark them when you backtrack.
  • Key Insight: Modifying the original grid (like replacing a character with #) is a space-efficient way to track visited cells without an extra visited matrix.

Visual:

    graph LR
    Cell["(r, c)"] --> Up["(r-1, c)"]
    Cell --> Down["(r+1, c)"]
    Cell --> Left["(r, c-1)"]
    Cell --> Right["(r, c+1)"]
    style Cell fill:#ccf,stroke:#333
  

Code:

function exist(board, word) {
    const m = board.length, n = board[0].length;

    function dfs(r, c, i) {
        if (i === word.length) return true;
        if (r < 0 || c < 0 || r >= m || c >= n || board[r][c] !== word[i]) return false;

        const temp = board[r][c];
        board[r][c] = '#';

        if (dfs(r+1, c, i+1) || dfs(r-1, c, i+1) || dfs(r, c+1, i+1) || dfs(r, c-1, i+1)) {
            return true;
        }

        board[r][c] = temp;
        return false;
    }

    for (let r = 0; r < m; r++) {
        for (let c = 0; c < n; c++) {
            if (dfs(r, c, 0)) return true;
        }
    }
    return false;
}

6. Generate Parentheses

LeetCode 22 | Difficulty: Medium

  • Brief: Given n pairs of parentheses, generate all combinations of well-formed parentheses.
  • Why this pattern?: You are building a string character by character. At each position, you can choose ( or ).
  • Key Insight: The search space is pruned by two rules. Only add ( if current open_count < n. Only add ) if current close_count < open_count.

Visual:

    graph TD
    Start["'' (0,0)"] --> L["'(' (1,0)"]
    L --> LL["'((' (2,0)"]
    L --> LR["'()' (1,1)"]
    LL --> LLR["'(()' (2,1)"]
    style Start fill:#eee
  

Code:

function generateParenthesis(n) {
    const result = [];
    function backtrack(s, open, close) {
        if (s.length === 2 * n) {
            result.push(s);
            return;
        }
        if (open < n) backtrack(s + "(", open + 1, close);
        if (close < open) backtrack(s + ")", open, close + 1);
    }
    backtrack("", 0, 0);
    return result;
}

Hard

7. Sudoku Solver

LeetCode 37 | Difficulty: Hard

  • Brief: Write a program to solve a Sudoku puzzle by filling the empty cells.
  • Why this pattern?: The number of possible configurations is astronomical. Backtracking lets you fill one cell, check if it is currently valid, and only then proceed to the next empty cell.
  • Key Insight: Upon finding an empty cell (.), try every digit from 1-9. If a digit leads to a solution, return true. Otherwise, reset to . and try the next digit.

Visual:

    graph TD
    A[Empty Cell] --> B[Try 1]
    B -- Valid --> C[Next Cell]
    B -- Invalid --> D[Try 2]
    C -- No path --> B
  

Code:

function solveSudoku(board) {
    function isValid(row, col, char) {
        for (let i = 0; i < 9; i++) {
            if (board[row][i] === char) return false;
            if (board[i][col] === char) return false;
            const blockRow = 3 * Math.floor(row / 3) + Math.floor(i / 3);
            const blockCol = 3 * Math.floor(col / 3) + (i % 3);
            if (board[blockRow][blockCol] === char) return false;
        }
        return true;
    }

    function solve() {
        for (let r = 0; r < 9; r++) {
            for (let c = 0; c < 9; c++) {
                if (board[r][c] === '.') {
                    for (let n = 1; n <= 9; n++) {
                        const char = n.toString();
                        if (isValid(r, c, char)) {
                            board[r][c] = char;
                            if (solve()) return true;
                            board[r][c] = '.';
                        }
                    }
                    return false;
                }
            }
        }
        return true;
    }
    solve();
}

8. N-Queens II

LeetCode 52 | Difficulty: Hard

  • Brief: Return the number of distinct solutions to the N-Queens puzzle.
  • Why this pattern?: You must place n queens such that no two queens attack each other. This requires checking row, column, and diagonal constraints at every step.
  • Key Insight: While rows are handled by recursion level, you can use sets or boolean arrays to track which columns and diagonals are already occupied.
    • Columns: index c.
    • Diagonals (positive slope): r + c.
    • Diagonals (negative slope): r - c.

Visual:

    graph TD
    Row0[Row 0] --> Q0["Q at (0,0)"]
    Q0 --> Row1[Row 1]
    Row1 --> Q1["Q at (1,2)"]
    Row1 --> Q2["Q at (1,3)"]
    style Row0 fill:#fcf
  

Code:

function totalNQueens(n) {
    let count = 0;
    const cols = new Set(), diag1 = new Set(), diag2 = new Set();

    function backtrack(row) {
        if (row === n) {
            count++;
            return;
        }
        for (let col = 0; col < n; col++) {
            if (cols.has(col) || diag1.has(row + col) || diag2.has(row - col)) continue;

            cols.add(col);
            diag1.add(row + col);
            diag2.add(row - col);

            backtrack(row + 1);

            cols.delete(col);
            diag1.delete(row + col);
            diag2.delete(row - col);
        }
    }
    backtrack(0);
    return count;
}
Hard problems are where patterns separate candidates who pass from candidates who get offers. Track your misses and review them on a spaced schedule so you do not repeat them in the real interview.

These eight problems cover the full range of backtracking techniques. Start with the basic choice pattern in Letter Case Permutation, work through the combinatorial problems, and finish with the complex constraint satisfaction in Sudoku and N-Queens. By the end, you should be able to recognize when backtracking applies and reach for the right template.

Done with these problems? The app has more, plus a spaced repetition system that brings problems back right before you would forget them. Continue your prep .