Backtracking

Backtracking builds an answer one choice at a time and undoes each choice to try the next, so it walks every branch of a tree of choices.

Subsets: every call records one

StopSearch tree
nums
the first call, recorded an answer, running[]✓
nums
  1. 1
  2. 2
  3. 3
pathempty
result
  1. []
start0
  • CH1 path
backtrack(0) starts with an empty path. Record [], the empty subset.
Typical cost Exponential in the worst case; pruning is what makes it tractableFull lesson: Subsets →

When to reach for it

  • You're asked for all of something: subsets, orderings, combinations, placements.
  • An answer is a sequence of choices, each from a small set.
  • Some partial answers can already be ruled out, so whole branches can be cut.
  • The output itself is exponential, so nothing much faster exists.

What stays true

Each choice is appended just before its call and popped just after the call returns. So when a call runs, the path holds its own choices and nothing left over from a sibling.

The path is the way down

StopSearch tree
InvariantThe path holds exactly the choices from the first call down to the running one.✓ Holds
the first call, running[]
nums
  1. 1
  2. 2
  3. 3
pathempty
result[]
  • CH1 path
Start with an empty path: every number is free.

Break it

Each input here has tripped up a version of this code. The stage runs the one that gets it right.

Recursing with i + 1 instead of i would allow 2 only once and miss [2, 2, 2, 2].

Backtracking on the inputs that trip it up

StopSearch tree
the first call, running8
candidates
  1. 2
pathempty
result[]
remaining8
  • CH1 path
Sort the candidates: [2]. Start with remaining = 8 and an empty path.
Answer[[2, 2, 2, 2]]Steps15

Variants

Permutations

StopSearch tree
the first call, returned[]call after choosing 1, returned1call after choosing 2, returned2call after choosing 3, recorded an answer3✓call after choosing 3, returned3call after choosing 2, recorded an answer2✓call after choosing 2, returned2call after choosing 1, returned1call after choosing 3, recorded an answer3✓call after choosing 3, returned3call after choosing 1, recorded an answer1✓call after choosing 3, returned3call after choosing 1, returned1call after choosing 2, recorded an answer2✓call after choosing 2, returned2call after choosing 1, recorded an answer1✓
nums
  1. 1
  2. 2
  3. 3
pathempty
result
  1. [1, 2, 3]
  2. [1, 3, 2]
  3. [2, 1, 3]
  4. [2, 3, 1]
  5. [3, 1, 2]
  6. [3, 2, 1]
  • CH1 path
53/53

Try every unused number at each place, and free it again on the way back.

Step through it →

Combination sum

StopSearch tree
2222363363336667the first call, returned7call after choosing 2, returned5call after choosing 2, returned3call after choosing 2, returned1call after choosing 2, pruned✗call after choosing 3, recorded an answer0✓call after choosing 6, pruned✗call after choosing 3, returned2call after choosing 3, pruned✗call after choosing 6, pruned✗call after choosing 3, returned4call after choosing 3, returned1call after choosing 3, pruned✗call after choosing 6, pruned✗call after choosing 6, returned1call after choosing 6, pruned✗call after choosing 7, recorded an answer0✓
candidates
  1. 2
  2. 3
  3. 6
  4. 7
pathempty
result
  1. [2, 2, 3]
  2. [7]
  • CH1 path
38/38

Recurse from the same index to reuse a candidate, and stop once one is too big.

Step through it →

The code

The same algorithm in plain Python. The lit line is the one the current frame came from.

Subsets: every call records one

StopSearch tree
the first call, recorded an answer, running[]✓
nums
  1. 1
  2. 2
  3. 3
pathempty
result
  1. []
start0
  • CH1 path
backtrack(0) starts with an empty path. Record [], the empty subset.
subsets.py
def subsets(nums):    result = []    path = []    def backtrack(start):        result.append(path[:])        for i in range(start, len(nums)):            path.append(nums[i])            backtrack(i + 1)            path.pop()    backtrack(0)    return result

Drill it