Pattern 18 of 24 · Search & optimisation · 35 min

Backtracking

Build a solution one choice at a time, recurse, then undo the choice: the template behind subsets, permutations and puzzles.

Spot it:all combinations / permutations / subsetsgenerate every validplace items with constraintsJavaScript you'll use: Recursion, the call stack and memoization Arrays: create, read, update Destructuring, spread and rest
Say it in 30 seconds
  • O(n · 2^n)All subsets of n items, copying each
  • O(n · n!)All permutations of n items
  • O(n)Extra space: path and used[], besides the output
Show what to say

“When a problem asks for all subsets, permutations or valid arrangements, or to place items under constraints, I backtrack: choose, explore everything after it, then un-choose, with one shared path. It is exponential, so I make each answer appear exactly once with a start index or used[], and prune a branch before recursing.”

Trade-offs

  • Start index vs used[]: when order does not matter a start index builds each combination once; when it does, used[] allows every arrangement.
  • Prevent duplicates by construction vs dedupe with a Set afterwards: the Set is slow and a sign the choice rule is wrong.
  • Backtracking vs DP: listing every solution is backtracking; counting them is DP, because paths reach the same subproblem and answers can be cached.

Bring it up before they ask

Undo every change after the recursive call (pop, used[i] = false, restore the cell), and record a copy, [...path], because the live array is empty by the end.

0 of 0 sections ticked
30-second answer

Backtracking builds an answer one choice at a time: choose, explore, then un-choose and try the next. It is slow by nature, so the craft is skipping dead ends early.

When to reach for it

Spot it in the problem0 of 5

Tap the signals you can see in the problem.

Rememberallevery validPlaceSmall limitsany

Pick your variant
  • Subsets, combinations

    Repeats avoided bya start index

    subsets
  • Permutations

    Repeats avoided bya used[] array

    permutations
  • Placement

    Repeats avoided byfixed slot order

    n-queens
  • Grid search

    Repeats avoided bymark, then restore

    word search

The core idea: one path, walked forward and back

Picture a maze with a ball of string.

Read more · 94 words

At each fork you pick a corridor and unroll the string. At a dead end you wind it back to the fork and try the next corridor. That is backtracking.

Every sequence of choices forms a tree: each edge is a choice, each node a partial answer. You walk it with one shared path:

  1. Choose: add an option to path.
  2. Explore: recurse into everything that follows.
  3. Un-choose: remove it, so path is as it was.

path always matches the highlighted route from the root:

Check two things: every answer appears, and each appears once.

Choose, explore, un-choose: the recursion tree

recursion tree
[]
path (0)
empty
result (size 0)
empty
n
4
k
2
start
1

Call backtrack(1) with an empty path: the root of the recursion tree. Every node is one call, and the path from the root to it is the list of choices made so far.

function combinations(n, k) {
  const result = [], path = [];
  function backtrack(start) {
    if (path.length === k) {
      result.push([...path]);
      return;
    }
    for (let i = start; i <= n; i++) {
      path.push(i);
      backtrack(i + 1);
      path.pop();
    }
  }
  backtrack(1);
  return result;
}
All steps as text
  1. Call backtrack(1) with an empty path: the root of the recursion tree. Every node is one call, and the path from the root to it is the list of choices made so far.
  2. Choose 1: push it, so the path is [1]. Explore with start = 2: only numbers bigger than 1 may follow, so no combination is built twice in another order.
  3. Choose 2: push it, so the path is [1, 2]. Explore with start = 3: only numbers bigger than 2 may follow, so no combination is built twice in another order.
  4. The path [1, 2] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  5. The call for [1, 2] has returned. Un-choose 2: pop it, so the path is [1] again and the next number can take its place.
  6. Choose 3: push it, so the path is [1, 3]. Explore with start = 4: only numbers bigger than 3 may follow, so no combination is built twice in another order.
  7. The path [1, 3] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  8. The call for [1, 3] has returned. Un-choose 3: pop it, so the path is [1] again and the next number can take its place.
  9. Choose 4: push it, so the path is [1, 4]. Explore with start = 5: only numbers bigger than 4 may follow, so no combination is built twice in another order.
  10. The path [1, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  11. The call for [1, 4] has returned. Un-choose 4: pop it, so the path is [1] again and the next number can take its place.
  12. The call for [1] has returned. Un-choose 1: pop it, so the path is [] again and the next number can take its place.
  13. Choose 2: push it, so the path is [2]. Explore with start = 3: only numbers bigger than 2 may follow, so no combination is built twice in another order.
  14. Choose 3: push it, so the path is [2, 3]. Explore with start = 4: only numbers bigger than 3 may follow, so no combination is built twice in another order.
  15. The path [2, 3] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  16. The call for [2, 3] has returned. Un-choose 3: pop it, so the path is [2] again and the next number can take its place.
  17. Choose 4: push it, so the path is [2, 4]. Explore with start = 5: only numbers bigger than 4 may follow, so no combination is built twice in another order.
  18. The path [2, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  19. The call for [2, 4] has returned. Un-choose 4: pop it, so the path is [2] again and the next number can take its place.
  20. The call for [2] has returned. Un-choose 2: pop it, so the path is [] again and the next number can take its place.
  21. Choose 3: push it, so the path is [3]. Explore with start = 4: only numbers bigger than 3 may follow, so no combination is built twice in another order.
  22. Choose 4: push it, so the path is [3, 4]. Explore with start = 5: only numbers bigger than 4 may follow, so no combination is built twice in another order.
  23. The path [3, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
  24. The call for [3, 4] has returned. Un-choose 4: pop it, so the path is [3] again and the next number can take its place.
  25. The call for [3] has returned. Un-choose 3: pop it, so the path is [] again and the next number can take its place.
  26. Choose 4: push it, so the path is [4]. Explore with start = 5: only numbers bigger than 4 may follow, so no combination is built twice in another order.
  27. start = 5 is past n = 4: no numbers are left to choose, but the path [4] is still shorter than k. A dead end: return without recording.
  28. The call for [4] has returned. Un-choose 4: pop it, so the path is [] again and the next number can take its place.
  29. The loop at the root has tried every first number: 6 combinations, each built exactly once. The grey call [4] was a dead end that could never reach 2 numbers; checking "are enough numbers left?" before recursing would prune it.

Play with it

Place N queens so none attack. Step through the search, then try Your turn.

Read more · 91 words

Step by step

  1. Define the partial solution and when it is complete

    A path array. Complete means length k, target hit or row n reached.

  2. List the choices at one step

    The next number, column or cell: the loop inside the recursive function.

  3. Rule out duplicates by construction

    Order irrelevant: start index. Order matters: used[].

  4. Write choose, explore, un-choose

    Undo every change right after the recursive call.

  5. Record copies

    result.push([...path]). Pushing path itself stores an array that ends up empty.

  6. Prune

    Ask "can this branch still succeed?" before recursing.

Interactive

N-Queens: choose, explore, un-choose

Place N queens so that none attack each other. Watch backtracking prune its way to every solution, or try it yourself.

Row 1 of 6 · dots = attackedStack depth 0/6row 1: try col 1
Solutions
0 of 4
Squares tried
0
Backtracks
0
Brute force
6⁶ = 46,656 boards

Step places queens row by row, trying columns left to right. A 6 × 6 board has 4 solutions.

Board size and speed

Templates

Template 1: combinations with a start index

Order doesn't matter, so pick items left to right. Each call gets a start and chooses from there on.

Record [...path] at the top of every call to list every subset: SubsetsMedium. Recurse with i instead of i + 1 to allow repeats: Combination SumMedium.

// Template 1: choose → explore → un-choose, with a start index.
// Lists every way to pick k numbers from 1..n. Each call only picks numbers bigger than the last one,
// so every combination is built exactly once ([1, 2] is never also built as [2, 1]).
export function combinations(n, k) {
  const result = [];
  const path = [];

  function backtrack(start) {
    if (path.length === k) {
      result.push([...path]);    // record a copy: path keeps changing after this
      return;
    }
    for (let i = start; i <= n; i++) {
      path.push(i);              // choose
      backtrack(i + 1);          // explore: only numbers after i
      path.pop();                // un-choose, so the next i starts from the same path
    }
  }

  backtrack(1);
  return result;
}

Template 2: permutations with used[]

Now [a, b] and [b, a] are different answers.

Read more · 28 words

Each call loops over every item and skips those already on the path, tracked in used.

Un-choosing has two parts: path.pop() and used[i] = false. Practice: PermutationsMedium.

Order matters: any unused item can go next

recursion tree
[]
items (3)
a
b
c
used (3)
false
false
false
path (0)
empty
result (size 0)
empty
k
2
depth
0
found
0

Start with an empty path and every used flag false. Unlike combinations, there is no start index: after choosing b, a may still come next.

function arrangements(items, k) {
  const result = [], path = [];
  const used = items.map(() => false);
  function backtrack() {
    if (path.length === k) {
      result.push([...path]);
      return;
    }
    for (let i = 0; i < items.length; i++) {
      if (used[i]) continue;
      used[i] = true; path.push(items[i]);
      backtrack();
      path.pop(); used[i] = false;
    }
  }
  backtrack();
  return result;
}
All steps as text
  1. Start with an empty path and every used flag false. Unlike combinations, there is no start index: after choosing b, a may still come next.
  2. Choose a: set used[0] = true and push it. The path is [a].
  3. Skip a: used[] says it is already in the path. Choose b: set used[1] = true and push it. The path is [a, b].
  4. The path [a, b] holds k = 2 items: record a copy and return.
  5. Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
  6. Skip a: used[] says it is already in the path. Choose c: set used[2] = true and push it. The path is [a, c].
  7. The path [a, c] holds k = 2 items: record a copy and return.
  8. Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
  9. Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
  10. Choose b: set used[1] = true and push it. The path is [b].
  11. Choose a: set used[0] = true and push it. The path is [b, a].
  12. The path [b, a] holds k = 2 items: record a copy and return.
  13. Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
  14. Skip b: used[] says it is already in the path. Choose c: set used[2] = true and push it. The path is [b, c].
  15. The path [b, c] holds k = 2 items: record a copy and return.
  16. Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
  17. Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
  18. Choose c: set used[2] = true and push it. The path is [c].
  19. Choose a: set used[0] = true and push it. The path is [c, a].
  20. The path [c, a] holds k = 2 items: record a copy and return.
  21. Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
  22. Choose b: set used[1] = true and push it. The path is [c, b].
  23. The path [c, b] holds k = 2 items: record a copy and return.
  24. Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
  25. Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
  26. Every item has been tried in every position: 6 ordered selections, one per leaf of the tree.
// Template 2: when order matters, every call may pick ANY item that is not already in the path.
// used[i] records whether items[i] is in the path, so the check is O(1). With k = items.length
// this lists every permutation.
export function arrangements(items, k = items.length) {
  const result = [];
  const path = [];
  const used = new Array(items.length).fill(false);

  function backtrack() {
    if (path.length === k) {
      result.push([...path]);
      return;
    }
    for (let i = 0; i < items.length; i++) {
      if (used[i]) continue;     // already placed in this path
      used[i] = true;            // choose
      path.push(items[i]);
      backtrack();               // explore
      path.pop();                // un-choose: undo both changes
      used[i] = false;
    }
  }

  backtrack();
  return result;
}

Template 3: pruning

Check before recursing: can this branch still succeed?

Read more · 42 words

On sorted input, once one number is too big, so is every later one: break. Red nodes never run.

Pruning by rule works the same way. Generate ParenthesesMedium adds ")" only while a "(" is open. N-QueensHard never places a queen on an attacked square.

Pruning: stop as soon as a branch can't work

recursion tree (badge = amount still missing)
[]5
nums (sorted) (4)
1
2
3
4
path (0)
empty
result (size 0)
empty
target
5
remaining
5
depth
0

Sort the numbers: [1, 2, 3, 4]. Start at the root with 5 still missing. Sorting is what makes pruning possible.

function subsetsWithSum(nums, target) {
  nums.sort((a, b) => a - b);
  const result = [], path = [];
  function backtrack(start, remaining) {
    if (remaining === 0) {
      result.push([...path]);
      return;
    }
    for (let i = start; i < nums.length; i++) {
      if (nums[i] > remaining) break;
      path.push(nums[i]);
      backtrack(i + 1, remaining - nums[i]);
      path.pop();
    }
  }
  backtrack(0, target);
  return result;
}
All steps as text
  1. Sort the numbers: [1, 2, 3, 4]. Start at the root with 5 still missing. Sorting is what makes pruning possible.
  2. Choose 1: the path is [1] and 4 is still missing. Explore with start = 1, so 1 isn't used twice.
  3. Choose 2: the path is [1, 2] and 2 is still missing. Explore with start = 2, so 2 isn't used twice.
  4. 3 is bigger than the 2 still missing, so this branch can't work. The numbers after it (4) are even bigger, so break: none of them is tried.
  5. Un-choose 2: the path is back to [1], with 4 missing. Try the next number.
  6. Choose 3: the path is [1, 3] and 1 is still missing. Explore with start = 3, so 3 isn't used twice.
  7. 4 is bigger than the 1 still missing, so this branch can't work. Break without making the call.
  8. Un-choose 3: the path is back to [1], with 4 missing. Try the next number.
  9. Choose 4: the path is [1, 4] and 0 is still missing. Explore with start = 4, so 4 isn't used twice.
  10. Nothing is missing: 1 + 4 = 5. Record a copy of the path and return.
  11. Un-choose 4: the path is back to [1], with 4 missing. Try the next number.
  12. Un-choose 1: the path is back to [], with 5 missing. Try the next number.
  13. Choose 2: the path is [2] and 3 is still missing. Explore with start = 2, so 2 isn't used twice.
  14. Choose 3: the path is [2, 3] and 0 is still missing. Explore with start = 3, so 3 isn't used twice.
  15. Nothing is missing: 2 + 3 = 5. Record a copy of the path and return.
  16. Un-choose 3: the path is back to [2], with 3 missing. Try the next number.
  17. 4 is bigger than the 3 still missing, so this branch can't work. Break without making the call.
  18. Un-choose 2: the path is back to [], with 5 missing. Try the next number.
  19. Choose 3: the path is [3] and 2 is still missing. Explore with start = 3, so 3 isn't used twice.
  20. 4 is bigger than the 2 still missing, so this branch can't work. Break without making the call.
  21. Un-choose 3: the path is back to [], with 5 missing. Try the next number.
  22. Choose 4: the path is [4] and 1 is still missing. Explore with start = 4, so 4 isn't used twice.
  23. No numbers are left after 4, and 1 is still missing: a dead end. Return.
  24. Un-choose 4: the path is back to [], with 5 missing. Try the next number.
  25. Search finished: 2 groups add up to 5. The red nodes are branches the break cut off before they were ever called.
// Template 3: pruning. Lists every group of numbers (each used at most once) that adds up to target.
// The numbers are positive and distinct. Sorting first means that once a number is too big for what
// is left, every number after it is too: break, and the whole rest of the loop is skipped.
export function subsetsWithSum(nums, target) {
  const sorted = [...nums].sort((a, b) => a - b);
  const result = [];
  const path = [];

  function backtrack(start, remaining) {
    if (remaining === 0) {
      result.push([...path]);
      return;
    }
    for (let i = start; i < sorted.length; i++) {
      if (sorted[i] > remaining) break;          // prune this branch and all the ones after it
      path.push(sorted[i]);                      // choose
      backtrack(i + 1, remaining - sorted[i]);   // explore: i + 1, each number at most once
      path.pop();                                // un-choose
    }
  }

  backtrack(0, target);
  return result;
}

Template 4: backtracking on a grid

On a grid the choices are the four neighbours. Overwrite the cell with a marker, explore, then write the original back. A cell on a failed path is free again.

Practice: Word SearchMedium.

// Template 4: backtracking on a grid. Counts the paths from the top-left to the bottom-right cell that
// step only on open cells (0), move up/down/left/right, and never visit a cell twice.
// The grid itself is the visited set: mark a cell before exploring from it, restore it afterwards.
const DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];

export function countPaths(grid) {
  const rows = grid.length;
  const cols = grid[0].length;

  function explore(r, c) {
    if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] !== 0) return 0; // off the board, wall or on the path
    if (r === rows - 1 && c === cols - 1) return 1;
    grid[r][c] = 2;                              // choose: this cell is on the current path
    let total = 0;
    for (const [dr, dc] of DIRS) total += explore(r + dr, c + dc); // explore
    grid[r][c] = 0;                              // un-choose: free the cell for other paths
    return total;
  }

  return explore(0, 0);
}

Try it

try-it.js
// Edit the inputs and press Run. The functions are the templates above.
console.log(combinations(4, 2));             // [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
console.log(arrangements(['a', 'b', 'c'], 2).map((p) => p.join(''))); // ['ab', 'ac', 'ba', 'bc', 'ca', 'cb']
console.log(subsetsWithSum([4, 1, 3, 2], 5)); // [[1, 4], [2, 3]]

const grid = [
  [0, 0, 0],
  [0, 1, 0],   // 1 is a wall
  [0, 0, 0],
];
console.log(countPaths(grid));               // 2: along the top and right, or down the left and along the bottom

Complexity

Estimate (nodes in the tree) × (work per node).

Read more · 34 words
ProblemTimeSpace
All subsets of n itemsO(n · 2^n)O(n)
All permutationsO(n · n!)O(n)
Word in an m × n gridO(m · n · 3^L)O(L)

JavaScript you'll use

JavaScript refresher: Recursion, the call stack and memoization Arrays: create, read, update Destructuring, spread and rest
Read more · 29 words
result.push([...path]);                    // record a COPY of the current path
path.push(x); backtrack(i + 1); path.pop(); // choose, explore, un-choose
const used = new Array(n).fill(false);     // flags for permutations
const word = path.join("");                // character path -> string answer
const board = Array.from({ length: n }, () => Array(n).fill(".")); // n independent rows

Interview questions

The input has duplicates. How do you avoid duplicate answers?

Sort, then skip nums[i] when it equals nums[i - 1] and that value was already tried at this level.

How is backtracking different from DFS and dynamic programming?

Backtracking is DFS over partial solutions, with an undo. DP applies when paths reach the same subproblem. Counting subsets is DP; listing them is backtracking.

How would you search a grid for many words at once?

Put the words in a trie and run one search per cell, following trie children.

Check yourself

Question 1 of 3

Score 2 out of 3 or better to mark this page complete.

1. Your subsets function returns 8 entries for [1, 2, 3], but every entry is an empty array. What is the bug?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

Next: Sometimes the locally best choice is provably globally best: greedy.