Backtracking
Build a solution one choice at a time, recurse, then undo the choice: the template behind subsets, permutations and puzzles.
- 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.
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
Tap the signals you can see in the problem.
Rememberallevery validPlaceSmall limitsany
- Subsets, combinations
Repeats avoided bya start index
subsets - Permutations
Repeats avoided bya
permutationsused[]array - 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:
- Choose: add an option to
path. - Explore: recurse into everything that follows.
- Un-choose: remove it, so
pathis 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
- 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
- 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.
- 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.
- 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.
- The path [1, 2] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- 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.
- The path [1, 3] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- 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.
- The path [1, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- The call for [1] has returned. Un-choose 1: pop it, so the path is [] again and the next number can take its place.
- 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.
- 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.
- The path [2, 3] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- 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.
- The path [2, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- The call for [2] has returned. Un-choose 2: pop it, so the path is [] again and the next number can take its place.
- 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.
- 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.
- The path [3, 4] has k = 2 numbers: record a copy and return. A copy, because path itself will keep changing.
- 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.
- The call for [3] has returned. Un-choose 3: pop it, so the path is [] again and the next number can take its place.
- 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.
- 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.
- The call for [4] has returned. Un-choose 4: pop it, so the path is [] again and the next number can take its place.
- 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
Define the partial solution and when it is complete
A
patharray. Complete means length k, target hit or row n reached.List the choices at one step
The next number, column or cell: the loop inside the recursive function.
Rule out duplicates by construction
Order irrelevant:
startindex. Order matters:used[].Write choose, explore, un-choose
Undo every change right after the recursive call.
Record copies
result.push([...path]). Pushingpathitself stores an array that ends up empty.Prune
Ask "can this branch still succeed?" before recursing.
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
- 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
- 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.
- Choose a: set used[0] = true and push it. The path is [a].
- Skip a: used[] says it is already in the path. Choose b: set used[1] = true and push it. The path is [a, b].
- The path [a, b] holds k = 2 items: record a copy and return.
- Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
- Skip a: used[] says it is already in the path. Choose c: set used[2] = true and push it. The path is [a, c].
- The path [a, c] holds k = 2 items: record a copy and return.
- Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
- Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
- Choose b: set used[1] = true and push it. The path is [b].
- Choose a: set used[0] = true and push it. The path is [b, a].
- The path [b, a] holds k = 2 items: record a copy and return.
- Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
- Skip b: used[] says it is already in the path. Choose c: set used[2] = true and push it. The path is [b, c].
- The path [b, c] holds k = 2 items: record a copy and return.
- Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
- Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
- Choose c: set used[2] = true and push it. The path is [c].
- Choose a: set used[0] = true and push it. The path is [c, a].
- The path [c, a] holds k = 2 items: record a copy and return.
- Un-choose a: pop it and set used[0] = false, so sibling branches can use a in a later position.
- Choose b: set used[1] = true and push it. The path is [c, b].
- The path [c, b] holds k = 2 items: record a copy and return.
- Un-choose b: pop it and set used[1] = false, so sibling branches can use b in a later position.
- Un-choose c: pop it and set used[2] = false, so sibling branches can use c in a later position.
- 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
- 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
- Sort the numbers: [1, 2, 3, 4]. Start at the root with 5 still missing. Sorting is what makes pruning possible.
- Choose 1: the path is [1] and 4 is still missing. Explore with start = 1, so 1 isn't used twice.
- Choose 2: the path is [1, 2] and 2 is still missing. Explore with start = 2, so 2 isn't used twice.
- 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.
- Un-choose 2: the path is back to [1], with 4 missing. Try the next number.
- Choose 3: the path is [1, 3] and 1 is still missing. Explore with start = 3, so 3 isn't used twice.
- 4 is bigger than the 1 still missing, so this branch can't work. Break without making the call.
- Un-choose 3: the path is back to [1], with 4 missing. Try the next number.
- Choose 4: the path is [1, 4] and 0 is still missing. Explore with start = 4, so 4 isn't used twice.
- Nothing is missing: 1 + 4 = 5. Record a copy of the path and return.
- Un-choose 4: the path is back to [1], with 4 missing. Try the next number.
- Un-choose 1: the path is back to [], with 5 missing. Try the next number.
- Choose 2: the path is [2] and 3 is still missing. Explore with start = 2, so 2 isn't used twice.
- Choose 3: the path is [2, 3] and 0 is still missing. Explore with start = 3, so 3 isn't used twice.
- Nothing is missing: 2 + 3 = 5. Record a copy of the path and return.
- Un-choose 3: the path is back to [2], with 3 missing. Try the next number.
- 4 is bigger than the 3 still missing, so this branch can't work. Break without making the call.
- Un-choose 2: the path is back to [], with 5 missing. Try the next number.
- Choose 3: the path is [3] and 2 is still missing. Explore with start = 3, so 3 isn't used twice.
- 4 is bigger than the 2 still missing, so this branch can't work. Break without making the call.
- Un-choose 3: the path is back to [], with 5 missing. Try the next number.
- Choose 4: the path is [4] and 1 is still missing. Explore with start = 4, so 4 isn't used twice.
- No numbers are left after 4, and 1 is still missing: a dead end. Return.
- Un-choose 4: the path is back to [], with 5 missing. Try the next number.
- 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
// 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 bottomComplexity
Estimate (nodes in the tree) × (work per node).
Read more · 34 words
| Problem | Time | Space |
|---|---|---|
| All subsets of n items | O(n · 2^n) | O(n) |
| All permutations | O(n · n!) | O(n) |
| Word in an m × n grid | O(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 restRead 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 rowsInterview 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 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- SubsetsList every subset of an array of distinct numbers, the empty subset included.Medium
- PermutationsReturn every possible ordering of an array of distinct numbers.Medium
- Combination SumFind every distinct way to reach a target sum using candidates that may be reused any number of times.Medium
- Generate ParenthesesGenerate every balanced string made of n pairs of parentheses.Medium
- Word SearchDecide whether a word can be traced through neighbouring cells of a letter grid without reusing a cell.Medium
- N-QueensPlace n queens on an n × n chessboard so no two attack each other, and return every such board.Hard
- Word Search IIFind every word from a list that can be traced through adjacent cells of a letter grid.Hard