Backtracking

Build answers one choice at a time, undo the choice, and try the next. One choose, explore, un-choose template for subsets, permutations and puzzles.

18 minLesson 1 of 5
On this page
  1. The idea: choose, explore, un-choose
  2. Subsets, traced
  3. Permutations: same template, different options
  4. A puzzle: prune early
  5. How much does it cost
  6. Common mistakes
  7. Check yourself
  8. What to practise next

A sandwich shop lets you add any mix of three extras: cheese, egg and tomato. You want to list every sandwich you could order, from plain bread to all three. With three extras you can do it in your head. With twelve there are 4,096, and you need a method that never misses one and never writes one twice.

A careful person does it like this. Add an extra. List everything you can still add on top. When you run out, take that extra back off and try a different one. That "add it, go deeper, take it back" rhythm is backtracking.

The idea: choose, explore, un-choose

Backtracking builds an answer one decision at a time. You keep a single path: the choices made so far. At every point you do three things for each option still available:

  1. Choose: add the option to the path.
  2. Explore: call yourself recursively to make the remaining decisions.
  3. Un-choose: remove the option, so the path is exactly what it was before, ready for the next option.

Each call is one decision point, and the call stack remembers where you were, as in Recursion. Un-choosing lets every branch share one path.

The calls form a tree. Each node is a path and each edge adds one extra. A call only offers extras after the last one chosen, so "egg then cheese" never shows up as a copy of "cheese then egg".

+C +E +T +E +T +T +T ∅ C E T CE CT ET CET a call only adds extras to the right of its last one
The tree of calls for three extras. Every node is a valid sandwich, and each one is reached exactly once.

Subsets, traced

Write cheese, egg and tomato as C, E and T. Watch the path grow and shrink. It holds one sandwich at a time; the recorded list collects copies.

Every sandwich from three extrasUse the arrows or ← → keys
explore(start):  record a copy of path  for i from start to n - 1:    path.push(extras[i])    # choose    explore(i + 1)          # explore    path.pop()              # un-choose
  1. extras
    0C
    1E
    2T
    path
    recorded
    0∅
    start
    0

    1explore(0) starts with an empty path. A sandwich with no extras counts, so record it.

  2. extras
    0Ci
    1E
    2T
    path
    0C
    recorded
    0∅
    start
    0

    2Choose cheese: push C onto the path.

  3. extras
    0C
    1E
    2T
    path
    0C
    recorded
    0∅
    1C
    start
    1

    3explore(1) records the path. C on its own is a sandwich.

  4. extras
    0C
    1Ei
    2T
    path
    0C
    1E
    recorded
    0∅
    1C
    start
    1

    4The loop in explore(1) starts at index 1, so the next choice is egg. Push E.

  5. extras
    0C
    1E
    2T
    path
    0C
    1E
    recorded
    0∅
    1C
    2CE
    start
    2

    5explore(2) records CE.

  6. extras
    0C
    1E
    2Ti
    path
    0C
    1E
    2T
    recorded
    0∅
    1C
    2CE
    start
    2

    6Choose tomato. Push T.

  7. extras
    0C
    1E
    2T
    path
    0C
    1E
    2T
    recorded
    0∅
    1C
    2CE
    3CET
    start
    3

    7explore(3) records CET. Its loop starts past the end, so it returns.

  8. extras
    0C
    1E
    2Ti
    path
    0C
    1E
    recorded
    0∅
    1C
    2CE
    3CET
    start
    2

    8Un-choose: pop T. The path is exactly what it was before T was chosen. explore(2) has no more options, so it returns.

  9. extras
    0C
    1Ei
    2T
    path
    0C
    recorded
    0∅
    1C
    2CE
    3CET
    start
    1

    9Pop E. Back in explore(1), the loop moves on to tomato.

  10. extras
    0C
    1E
    2Ti
    path
    0C
    1T
    recorded
    0∅
    1C
    2CE
    3CET
    start
    1

    10Choose tomato: push T. The path is CT.

  11. extras
    0C
    1E
    2T
    path
    0C
    1T
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    start
    3

    11explore(3) records CT and returns at once.

  12. extras
    0C
    1E
    2Ti
    path
    0C
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    start
    1

    12Pop T. explore(1) has tried every extra after cheese, so it returns.

  13. extras
    0Ci
    1E
    2T
    path
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    start
    0

    13Pop C. Every sandwich with cheese is recorded. explore(0) moves on to egg.

  14. extras
    0C
    1Ei
    2T
    path
    0E
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    start
    0

    14Choose egg. Cheese is never offered again, because the loop only looks to the right. That is why EC never appears as a copy of CE.

  15. extras
    0C
    1E
    2T
    path
    0E
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    start
    2

    15explore(2) records E.

  16. extras
    0C
    1E
    2Ti
    path
    0E
    1T
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    start
    2

    16Choose tomato. The path is ET.

  17. extras
    0C
    1E
    2T
    path
    0E
    1T
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    start
    3

    17explore(3) records ET.

  18. extras
    0C
    1E
    2Ti
    path
    0E
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    start
    2

    18Pop T. explore(2) is done.

  19. extras
    0C
    1Ei
    2T
    path
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    start
    0

    19Pop E. explore(0) moves on to tomato, the last extra.

  20. extras
    0C
    1E
    2Ti
    path
    0T
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    start
    0

    20Choose tomato. The path is T.

  21. extras
    0C
    1E
    2T
    path
    0T
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    7T
    start
    3

    21explore(3) records T.

  22. extras
    0C
    1E
    2Ti
    path
    recorded
    0∅
    1C
    2CE
    3CET
    4CT
    5E
    6ET
    7T
    start
    0

    22Pop T. Every loop has finished: 8 sandwiches, 2 × 2 × 2 = 2³, because each extra is in or out.

1 / 22

Look at step 8. After popping T, the path is CE again. Every call leaves the path as it found it, so the caller's loop can carry on as if nothing happened.

All subsets
function subsets(items) {
  const result = [];
  const path = [];
  function explore(start) {
    result.push([...path]); // record a copy, not path itself
    for (let i = start; i < items.length; i++) {
      path.push(items[i]);  // choose
      explore(i + 1);       // explore
      path.pop();           // un-choose
    }
  }
  explore(0);
  return result;
}
 
subsets(["C", "E", "T"]);
// [[], ["C"], ["C","E"], ["C","E","T"], ["C","T"], ["E"], ["E","T"], ["T"]]
def subsets(items):
    result = []
    path = []
 
    def explore(start):
        result.append(path[:])  # record a copy, not path itself
        for i in range(start, len(items)):
            path.append(items[i])  # choose
            explore(i + 1)         # explore
            path.pop()             # un-choose
 
    explore(0)
    return result
 
subsets(["C", "E", "T"])
# [[], ['C'], ['C', 'E'], ['C', 'E', 'T'], ['C', 'T'], ['E'], ['E', 'T'], ['T']]

Permutations: same template, different options

Now three friends, Ana, Ben and Cy, need a speaking order. Order matters here. Two things change:

  • The options are everyone not used yet, tracked in a used list.
  • You record only when the path is full. Half an order is not an answer.

By hand: Ana, Ben, Cy is the first full path. Un-choose Cy and Ben, put Cy second, and you get Ana, Cy, Ben. Then Ben goes first, then Cy.

All orderings
function permutations(items) {
  const result = [];
  const path = [];
  const used = new Array(items.length).fill(false);
  function explore() {
    if (path.length === items.length) {
      result.push([...path]);
      return;
    }
    for (let i = 0; i < items.length; i++) {
      if (used[i]) continue;
      used[i] = true;     // choose
      path.push(items[i]);
      explore();          // explore
      path.pop();         // un-choose
      used[i] = false;
    }
  }
  explore();
  return result;
}
 
permutations(["Ana", "Ben", "Cy"]).length; // 6
def permutations(items):
    result = []
    path = []
    used = [False] * len(items)
 
    def explore():
        if len(path) == len(items):
            result.append(path[:])
            return
        for i in range(len(items)):
            if used[i]:
                continue
            used[i] = True          # choose
            path.append(items[i])
            explore()               # explore
            path.pop()              # un-choose
            used[i] = False
 
    explore()
    return result
 
len(permutations(["Ana", "Ben", "Cy"]))  # 6

Choosing now makes two updates, so un-choosing undoes both. Three choices for first place, two for second, one for third: 3 × 2 × 1 = 6 orders.

A puzzle: prune early

Backtracking pays off most when some choices break a rule. Place 4 queens on a 4 × 4 board so that no two share a row, column or diagonal. Put one queen per row, so each call picks the column for one row.

The new ingredient is the prune: skip any square that is already attacked. A bad partial board is dropped the moment it goes wrong, along with every board that would have grown from it.

Four queens, one row at a timeUse the arrows or ← → keys
explore(row):  if row == n: record placed; return  for col from 0 to n - 1:    if col or a diagonal is taken: skip   # prune    place queen at (row, col)             # choose    explore(row + 1)                      # explore    remove queen from (row, col)          # un-choose
  1. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    0
    placed
    [0]

    1Row 0: the board is empty, so column 0 is safe. Place a queen in the corner.

  2. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    1
    placed
    [0]

    2Row 1: column 0 is taken and column 1 is on a diagonal. Skip both.

  3. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    1
    placed
    [0, 2]

    3Column 2 is safe. Place the second queen.

  4. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    2
    placed
    [0, 2]

    4Row 2: every square is attacked. The loop ends without placing anything. Dead end.

  5. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    1
    placed
    [0]

    5Un-choose: take the queen off row 1, column 2. The board is back to how it was before that choice.

  6. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    1
    placed
    [0, 3]

    6Row 1 tries column 3. It is safe, so place a queen.

  7. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    2
    placed
    [0, 3]

    7Row 2: column 0 is taken by the first queen. Skip it.

  8. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3
    row
    2
    placed
    [0, 3, 1]

    8Column 1 is safe. Place the third queen.

  9. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3
    row
    3
    placed
    [0, 3, 1]

    9Row 3: all four squares are attacked. Another dead end.

  10. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    2
    placed
    [0, 3]

    10Remove the queen from row 2. Columns 2 and 3 are attacked too, so row 2 gives up.

  11. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    1
    placed
    [0]

    11Remove the queen from row 1, column 3. Row 1 has no columns left to try.

  12. 0123
    row 0
    row 1
    row 2
    row 3
    row
    0
    placed
    []

    12Remove the corner queen. No answer has a queen in that corner, and you proved it without filling a single full board.

  13. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    0
    placed
    [1]

    13Row 0 tries column 1.

  14. 0123
    row 0Q
    row 1
    row 2
    row 3
    row
    1
    placed
    [1]

    14Row 1: columns 0 and 2 are on diagonals, column 1 is taken.

  15. 0123
    row 0Q
    row 1Q
    row 2
    row 3
    row
    1
    placed
    [1, 3]

    15Column 3 is safe. Place.

  16. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3
    row
    2
    placed
    [1, 3, 0]

    16Row 2: column 0 is safe on the first try. Place.

  17. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3
    row
    3
    placed
    [1, 3, 0]

    17Row 3: columns 0 and 1 are both taken. Skip them.

  18. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3Q
    row
    3
    placed
    [1, 3, 0, 2]

    18Column 2 is safe. Place the fourth queen.

  19. 0123
    row 0Q
    row 1Q
    row 2Q
    row 3Q
    row
    4
    placed
    [1, 3, 0, 2]

    19explore(4): every row has a queen, so record the solution. Only 8 queens were placed on the way here. Putting one queen in each row without pruning gives 4 × 4 × 4 × 4 = 256 boards to check.

1 / 19

To check a square in O(1), keep three sets: used columns, used row - col values (down-right diagonals) and used row + col values (down-left diagonals). Squares (0, 0) and (1, 1) both have row - col = 0, which is why step 2 skipped (1, 1).

Place n queens
function solveQueens(n) {
  const cols = new Set(), down = new Set(), up = new Set();
  const placed = []; // placed[row] = column of that row's queen
  const solutions = [];
  function explore(row) {
    if (row === n) { solutions.push([...placed]); return; }
    for (let col = 0; col < n; col++) {
      // prune: column or diagonal already taken
      if (cols.has(col) || down.has(row - col) || up.has(row + col)) continue;
      // choose
      cols.add(col); down.add(row - col); up.add(row + col);
      placed.push(col);
      explore(row + 1); // explore
      // un-choose
      cols.delete(col); down.delete(row - col); up.delete(row + col);
      placed.pop();
    }
  }
  explore(0);
  return solutions;
}
 
solveQueens(4); // [[1, 3, 0, 2], [2, 0, 3, 1]]
def solve_queens(n):
    cols, down, up = set(), set(), set()
    placed = []  # placed[row] = column of that row's queen
    solutions = []
 
    def explore(row):
        if row == n:
            solutions.append(placed[:])
            return
        for col in range(n):
            if col in cols or row - col in down or row + col in up:
                continue  # prune
            cols.add(col)  # choose
            down.add(row - col)
            up.add(row + col)
            placed.append(col)
            explore(row + 1)  # explore
            cols.remove(col)  # un-choose
            down.remove(row - col)
            up.remove(row + col)
            placed.pop()
 
    explore(0)
    return solutions
 
solve_queens(4)  # [[1, 3, 0, 2], [2, 0, 3, 1]]

How much does it cost

Backtracking visits every node of its call tree, so count the nodes. Subsets have 2ⁿ, because each item is in or out. Orderings have n! leaves. Copying each answer costs O(n). These numbers explode: 2²⁰ is about a million and 12! is about 479 million. Backtracking is for small inputs, or for problems where pruning cuts most of the tree away.

ApproachTimeSpaceWhy
All subsetsO(n · 2ⁿ)O(n)2ⁿ subsets, each copied in O(n). The path and the call stack are at most n deep, not counting the output.
All orderingsO(n · n!)O(n)n! full paths, each copied in O(n).
n queensO(n!) at mostO(n)At most n choices for row 0, n − 1 for row 1, and so on. Pruning cuts far below that in practice.

Common mistakes

Check yourself

Quick check

In the subsets code, what goes wrong if the loop starts at 0 instead of start?

A permutation search on 5 items records an answer only when the path is full. How many answers does it record?

Why does backtracking un-choose after the recursive call returns?

What to practise next

Backtracking tries every option. When you can prove one option is always best, you can skip the rest: that is Greedy Algorithms. When the same smaller question gets asked again and again inside the search, dynamic programming stores the answers instead.