Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 6 min read

Backtracking

The pattern: Build a solution incrementally, making one choice at a time. If a choice leads to a dead end, undo it (backtrack) and try the next option. It’s a controlled brute-force that prunes bad paths early.

Why this matters in interviews: Backtracking solves permutations, combinations, subsets, constraint-satisfaction (N-Queens, Sudoku), and word search. The template is nearly identical across all these problems β€” learn it once, apply it everywhere.


When to Recognize It


How It Works

Think of it as exploring a maze. At each fork, you pick a direction. If you hit a wall (invalid state), you go back to the last fork and try a different direction. You keep doing this until you’ve explored every viable path.

flowchart TD
    A["Start: empty choice"]:::client
    B["Choose 1"]:::service
    C["Choose 2"]:::service
    D["Choose 3"]:::service
    E["1,2"]:::data
    F["1,3"]:::data
    G["2,3"]:::data
    H["Prune: invalid"]:::client

    A --> B
    A --> C
    A --> D
    B --> E
    B --> F
    C --> G
    C --> H

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

The backtracking framework:

  1. Choose β€” pick an option from the remaining choices
  2. Explore β€” recurse with that choice added
  3. Unchoose β€” remove the choice (backtrack) and try the next option

Template Code

Code

def backtrack(result, path, choices):
    """Generic backtracking template."""
    if is_solution(path):
        result.append(path[:])  # save a copy
        return

    for choice in choices:
        if not is_valid(choice, path):
            continue  # prune

        path.append(choice)        # choose
        backtrack(result, path, next_choices(choice))  # explore
        path.pop()                 # unchoose (backtrack)

# Subsets: generate all subsets of nums
def subsets(nums):
    result = []
    def bt(start, path):
        result.append(path[:])
        for i in range(start, len(nums)):
            path.append(nums[i])
            bt(i + 1, path)
            path.pop()
    bt(0, [])
    return result
// Subsets
List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(result, new ArrayList<>(), nums, 0);
    return result;
}

void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int start) {
    result.add(new ArrayList<>(path));
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);
        backtrack(result, path, nums, i + 1);
        path.remove(path.size() - 1);
    }
}
// Subsets
vector<vector<int>> subsets(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> path;
    function<void(int)> bt = [&](int start) {
        result.push_back(path);
        for (int i = start; i < nums.size(); i++) {
            path.push_back(nums[i]);
            bt(i + 1);
            path.pop_back();
        }
    };
    bt(0);
    return result;
}
// Subsets
function subsets(nums) {
    const result = [];
    function bt(start, path) {
        result.push([...path]);
        for (let i = start; i < nums.length; i++) {
            path.push(nums[i]);
            bt(i + 1, path);
            path.pop();
        }
    }
    bt(0, []);
    return result;
}

Variations

Permutations

Every element must be used exactly once. Track which elements are already used.

Code

def permutations(nums):
    result = []
    def bt(path, used):
        if len(path) == len(nums):
            result.append(path[:])
            return
        for i in range(len(nums)):
            if used[i]:
                continue
            used[i] = True
            path.append(nums[i])
            bt(path, used)
            path.pop()
            used[i] = False
    bt([], [False] * len(nums))
    return result
List<List<Integer>> permutations(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    boolean[] used = new boolean[nums.length];
    bt(result, new ArrayList<>(), nums, used);
    return result;
}

void bt(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] used) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;
        used[i] = true;
        path.add(nums[i]);
        bt(result, path, nums, used);
        path.remove(path.size() - 1);
        used[i] = false;
    }
}
vector<vector<int>> permutations(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> path;
    vector<bool> used(nums.size(), false);
    function<void()> bt = [&]() {
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i]) continue;
            used[i] = true;
            path.push_back(nums[i]);
            bt();
            path.pop_back();
            used[i] = false;
        }
    };
    bt();
    return result;
}
function permutations(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);
    function bt(path) {
        if (path.length === nums.length) {
            result.push([...path]);
            return;
        }
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            path.push(nums[i]);
            bt(path);
            path.pop();
            used[i] = false;
        }
    }
    bt([]);
    return result;
}

Combination Sum (Reuse Allowed)

Allow picking the same element multiple times. Instead of start = i + 1, recurse with start = i.

N-Queens (Constraint Satisfaction)

Place queens one row at a time. For each row, try every column. Prune if the column, diagonal, or anti-diagonal is already attacked.


Complexity

Problem Time Space
Subsets O(2^n) O(n) recursion depth
Permutations O(n!) O(n)
Combination Sum O(2^t) where t = target/min O(target/min)
N-Queens O(n!) O(n)

These are exponential β€” that’s expected. Backtracking doesn’t make them polynomial; it just prunes dead branches so you explore far fewer than the theoretical maximum.


Common Mistakes


Practice Problems


Key Takeaways

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access