← All concepts
Recursion & Backtracking

Recursion & Backtracking

Backtracking explores a choice by recursing into it, and undoes that choice the moment it leads nowhere, so it can try the next one.

Definition

Recursion solves a problem by having a function call itself on a smaller version of the same problem, with a base case that stops the recursion once the problem is small enough to answer directly. Backtracking builds on recursion to search through a space of choices: at each step it picks one option, recurses to explore the consequences of that choice, and if the recursive call reaches a dead end, a state that cannot possibly lead to a valid solution, it undoes the choice and tries the next option instead. That undo step, often as simple as popping the last choice off an array or clearing a visited flag, is what separates backtracking from plain recursion: without it, the state from an abandoned branch would leak into the sibling branches tried afterward. Because each call frame captures the state made at that decision point, the call stack naturally tracks the sequence of choices made so far and unwinds it automatically when a function returns, so returning from a recursive call is often exactly the undo step a backtracking algorithm needs. Backtracking is used to enumerate or search a solution space that is too large to check exhaustively in advance, such as all permutations of a list, all ways to place non-attacking queens on a board, or all paths through a maze, and it prunes branches early whenever it can prove a partial choice cannot possibly succeed, which avoids wasting time exploring the rest of a doomed branch.

Examples

function subsets(nums) {
  const result = [];
  function backtrack(start, current) {
    result.push([...current]);
    for (let i = start; i < nums.length; i++) {
      current.push(nums[i]);
      backtrack(i + 1, current);
      current.pop(); // undo the choice
    }
  }
  backtrack(0, []);
  return result;
}
subsets([1, 2]); // [[], [1], [1, 2], [2]]

current.push adds a choice and recurses to explore it; current.pop right after is the backtrack step, removing that choice so the next iteration of the loop starts from a clean state.

function permute(nums) {
  const result = [], used = new Set();
  function backtrack(path) {
    if (path.length === nums.length) { result.push([...path]); return; }
    for (const n of nums) {
      if (used.has(n)) continue;
      used.add(n);
      path.push(n);
      backtrack(path);
      path.pop();
      used.delete(n); // undo before trying the next candidate
    }
  }
  backtrack([]);
  return result;
}
permute([1, 2, 3]).length; // 6

Marking a number used lets the recursion skip it in deeper calls; deleting it again after the recursive call returns undoes that mark so a sibling branch can use the same number.

function hasPath(grid, r, c, visited = new Set()) {
  const key = `${r},${c}`;
  if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length) return false;
  if (grid[r][c] === 1 || visited.has(key)) return false; // dead end
  if (r === grid.length - 1 && c === grid[0].length - 1) return true;
  visited.add(key);
  const found = hasPath(grid, r + 1, c, visited) || hasPath(grid, r, c + 1, visited);
  visited.delete(key); // backtrack so other paths can revisit this cell
  return found;
}

A wall or an already-visited cell is treated as a dead end and returns false immediately; visited.delete undoes marking the cell once this branch's exploration is finished, so a different path is free to consider it again.

Common mistakes

  • Forgetting the undo step, such as leaving a value pushed onto an array or a flag marked used, so state from an abandoned branch leaks into the next branch tried.
  • Missing or incorrect base case, causing recursion to continue past a valid dead end or solution instead of stopping.
  • Not pruning early enough, exploring an entire doomed branch to its end before checking a condition that could have ruled it out immediately.

Key takeaways

  • Backtracking is recursion plus an explicit undo step for whichever choice was just tried.
  • A dead end should stop the current branch immediately and trigger backtracking, not continue exploring further.
  • The call stack unwinding on return often performs the undo automatically, but any state mutated outside the call, like an array or set, must be undone manually.
  • Pruning a branch as soon as it can be proven invalid avoids wasted work exploring the rest of it.
Check your understanding

In a backtracking subset search, current.push(nums[i]) is followed later by current.pop(). What would happen if the pop() were removed?

Where you see this

  • Generating all permutations, subsets, or combinations of a set of items.
  • Solving constraint satisfaction puzzles such as Sudoku or the N-Queens problem.
  • Searching a maze or grid for a path, undoing a step whenever it leads to a dead end.
  • Parsing or compiling, where a parser tries one grammar rule, backs out if it fails, and tries another.

Practice this

Further reading

Previous
Dynamic Programming
Next
Greedy Algorithms