What is backtracking?
- Write down the plain pizza — no toppings still counts.
- Put mushroom on. That's a pizza — write it down.
- Keep the mushroom and add pepperoni — write down mushroom + pepperoni.
- Keep both and add olives — write down the works. Nothing left to add.
- Stuck. So take the olives back off and look for another topping to try after pepperoni — there is none. Take the pepperoni off too, and try olives straight after mushroom: write down mushroom + olives.
- Keep undoing-and-retrying the same way — pepperoni, pepperoni + olives, olives — until nothing is left to try. 8 pizzas, none missed, none repeated.
This section has a step-by-step animation in the interactive lesson.
Quick check
In plain words, what does backtracking do after it finishes exploring one choice?
Answer it in the interactive lesson to keep your progress.
How it works, step by step
['mush', 'pep', 'oli'].The recipe
- Keep a current pizza — the toppings chosen so far. This is the stack.
- Every state of that pizza is a valid order, so record it the moment you arrive.
- For each topping you haven't considered yet: choose it (
push), explore what can follow (recurse), then un-choose it (pop) and move to the next topping.
Trace it, choice by choice
| step | move | pizza (the stack) | recorded |
|---|---|---|---|
| 1 | start | [] | plain pizza |
| 2 | push mush | [mush] | mushroom |
| 3 | push pep | [mush, pep] | mushroom + pepperoni |
| 4 | push oli | [mush, pep, oli] | the works |
| 5 | pop oli, pop pep | [mush] | — (rewinding) |
| 6 | push oli | [mush, oli] | mushroom + olives |
| 7 | pop oli, pop mush | [] | — (rewinding) |
| 8 | push pep… | [pep] | pepperoni |
Every Pizza in JavaScript
function everyPizza(toppings) {
const result = [];
function backtrack(start, pizza) {
result.push([...pizza]); // record a COPY of the pizza so far
for (let i = start; i < toppings.length; i++) {
pizza.push(toppings[i]); // choose
backtrack(i + 1, pizza); // explore
pizza.pop(); // un-choose — the backtrack
}
}
backtrack(0, []);
return result;
}
console.log(everyPizza(['mush', 'pep', 'oli']).length); // 8
console.log(everyPizza(['mush', 'pep', 'oli'])[3]); // ['mush','pep','oli']Quick check
The pizza is [mush, pep, oli] and no toppings remain to try. What does it look like two pops later?
Answer it in the interactive lesson to keep your progress.
Same rhythm, new problem: the photo lineup
[Ana, Ben] and [Ben, Ana] are different lineups, and a lineup only counts when everyone is in it.- The choice rule. A pizza never repeats a topping and never looks back (
startmoves forward). A lineup can pick anyone who isn't already standing in it — so instead of a start index, we track who's still remaining. - The recording rule. Every half-built pizza was a valid order, so we recorded at every step. A half-built lineup is not a photo — we record only when nobody is left to place.
push, same pop, same backtrack.Photo Lineups in JavaScript
function lineups(people) {
const result = [];
function backtrack(row, remaining) {
if (remaining.length === 0) { result.push([...row]); return; } // photo complete
for (let i = 0; i < remaining.length; i++) {
row.push(remaining[i]); // choose
backtrack(row, [...remaining.slice(0, i), ...remaining.slice(i + 1)]);
row.pop(); // un-choose
}
}
backtrack([], people);
return result;
}
console.log(lineups(['Ana', 'Ben', 'Cai']).length); // 6
console.log(lineups(['Ana', 'Ben', 'Cai'])[0]); // ['Ana','Ben','Cai']Quick check
Why does the lineup version record results only when `remaining` is empty, while the pizza version recorded at every step?
Answer it in the interactive lesson to keep your progress.
Where it breaks (and how not to get burned)
- Forgetting to undo. If you push a choice and recurse but never pop, the leftover choice leaks into every path explored after it, and all the results are wrong. The undo is not cleanup — it is the algorithm.
- Storing a reference, not a copy.
result.push(path)stores the live array — the one you keep pushing to and popping from. Every stored "result" ends up pointing at the same final state. Store a snapshot:result.push([...path]). - Exploring hopeless branches. If you can already tell a path can't succeed (the sum is too big, the queen is attacked), stop there instead of exploring everything below it. This early cut is called pruning, and on big inputs it's the difference between fast and forever.
Quick check
Storing `path` itself instead of a copy in the results causes…
Answer it in the interactive lesson to keep your progress.
How fast is it? (complexity)
| Problem shape | How the tree grows | Space |
|---|---|---|
| include-or-skip (the pizzas) | O(2ⁿ) — doubles with each item | O(n) path + recursion |
| orderings (the photo lineup) | O(n · n!) — multiplies with each item | O(n) path + recursion |
O(2ⁿ) means the work doubles every time the menu grows by one; O(n!) grows even faster. That explosive growth is exactly why pruning matters — every branch you cut early removes an entire subtree of wasted work.- The pattern is always choose → explore → un-choose.
- The undo (pop) is the algorithm; snapshot with
[...path]before recording. - The recording rule belongs to the problem: record every stop (subsets) or only complete answers (orderings).
- Cost tracks the decision tree (
2ⁿ,n!); prune to shrink it.