Skip to content
DSA Patterns

Learn

Backtracking

One backtracking template for subsets, permutations, combinations and N-Queens: choose, recurse, un-choose — plus how to prune and how to skip duplicates.

3 min readUpdated 2 Sept 2026

#Recursion#Combinatorics#Pruning#DFS

Backtracking is DFS over a tree of decisions that does not exist in memory — you build a partial answer, extend it, and undo the extension when the branch is exhausted. Every "generate all…" question is this template, and the difference between candidates is almost never the recursion; it is the pruning and the duplicate handling.

The template

def backtrack(path, choices):
    if is_complete(path):
        results.append(path[:])      # COPY — path keeps mutating
        return

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

        path.append(choice)          # choose
        backtrack(path, next_choices(choice))
        path.pop()                   # un-choose

Three things to internalise:

  1. `path[:]`, not `path`. Appending the live list stores a reference that is empty by

the time recursion unwinds. This is the single most common backtracking bug.

  1. Every `append` has a matching `pop`. If a branch can return early, the pop must

still happen — keep them adjacent around the recursive call.

  1. Pruning is the whole performance story. The search space is exponential; cutting a

branch at depth 2 removes everything beneath it.

Subsets: include or exclude

def subsets(nums):
    out, path = [], []

    def go(start):
        out.append(path[:])              # every node is an answer, not just leaves
        for i in range(start, len(nums)):
            path.append(nums[i])
            go(i + 1)                    # i + 1: never reuse an element
            path.pop()

    go(0)
    return out

The start index is what stops [1,2] and [2,1] both appearing — subsets are unordered, so each element is only ever considered after the ones before it.

Permutations: order matters

Now every unused element is a candidate at every position, so start is replaced by a used-marker:

def permute(nums):
    out, path = [], []
    used = [False] * len(nums)

    def go():
        if len(path) == len(nums):
            out.append(path[:])
            return
        for i, x in enumerate(nums):
            if used[i]:
                continue
            used[i] = True
            path.append(x)
            go()
            path.pop()
            used[i] = False

    go()
    return out

The three index rules

Almost every combinatorial question is one of these, and picking the wrong one silently produces duplicates or misses answers:

Recurse withEffectExample
go(i + 1)each element used at most onceSubsets, Combinations
go(i)elements may repeatCombination Sum
used[] flags, loop from 0order mattersPermutations

Skipping duplicates

With repeated values in the input, sort first and skip a candidate equal to its predecessor at the same depth:

nums.sort()
for i in range(start, len(nums)):
    if i > start and nums[i] == nums[i - 1]:
        continue                     # same value already tried at this level

i > start is doing the real work: it allows a duplicate deeper in the path ([1,1] is a legitimate subset of [1,1,2]) while blocking two branches at the same level that would generate identical subtrees.

N-Queens: pruning with the right state

The brute force is 8⁸. What makes N-Queens tractable is checking conflicts in O(1) by tracking three sets instead of scanning the board:

def solve_n_queens(n):
    cols, diag, anti = set(), set(), set()
    board, out = [], []

    def go(row):
        if row == n:
            out.append(board[:])
            return
        for col in range(n):
            if col in cols or (row - col) in diag or (row + col) in anti:
                continue
            cols.add(col); diag.add(row - col); anti.add(row + col)
            board.append(col)
            go(row + 1)
            board.pop()
            cols.remove(col); diag.remove(row - col); anti.remove(row + col)

    go(0)
    return out

row - col is constant along a ↘ diagonal and row + col along a ↙ one. Placing one queen per row is itself a pruning decision — it removes every arrangement with two queens in a row without ever generating them.

Complexity

Exponential by nature, and you are expected to state which one: subsets O(2ⁿ × n), permutations O(n! × n), combination sum roughly O(2^target), N-Queens O(n!) before pruning. The trailing × n is the cost of copying each answer. Space is O(depth) for the stack plus the output.

Mistakes that cost the round

  • Appending `path` instead of `path[:]`. Every stored answer ends up empty.
  • Forgetting to un-choose, so state leaks into sibling branches.
  • Wrong index rulego(i) where go(i + 1) was needed produces infinite or

duplicated results.

  • Deduplicating at the end instead of pruning during. It works and it is slow, and it

is the answer that draws follow-ups.

  • Skipping with `i > 0` instead of `i > start`, which wrongly kills legitimate

repeats deeper in the path.

What to drill

  1. Subsets — the base template.
  2. Permutations — the used[] variant.
  3. Combination Sum — reuse allowed, prune on the running sum.
  4. Letter Combinations of a Phone Number — a mapped choice set.
  5. Palindrome Partitioning — validity check before recursing.
  6. N-Queens — O(1) conflict checks.
  7. Sudoku Solver — the same, with constraint propagation.

All on the 22 DSA Patterns sheet.

Frequently asked

What is the backtracking template?

Choose, recurse, un-choose. Append a candidate to the current path, recurse on the remaining choices, then pop it before trying the next candidate. Record a copy of the path when it is complete — a copy, because the path list keeps mutating as the recursion unwinds.

How do I avoid duplicate results in backtracking?

Sort the input, then inside the loop skip any candidate where i > start and nums[i] == nums[i-1]. That blocks two identical branches at the same depth while still allowing a repeated value deeper in the same path, which is usually a legitimate answer.

What is the difference between backtracking and DFS?

Backtracking is DFS over an implicit tree of decisions, with an explicit undo step. Plain DFS marks nodes visited permanently because it is exploring a fixed structure; backtracking un-marks on the way out because a choice blocked for one path must be available to another.

Related