Level Up Your Coding Skills & Crack Interviews — Save up to 50% or more on Educative.io Today! Claim Discount

Arrow
Table of contents

Backtracking

In coding interviews, many problems feel challenging not because the underlying logic is complicated, but because the search space is enormous. A brute-force “try every possibility” approach often leads to exponential runtime, complex branching, and errors introduced by managing mutable state across attempts.

Backtracking is the standard interview pattern for these problems. It gives you a disciplined way to explore a decision tree: build a candidate step by step, reject it as soon as it becomes invalid, and revert your last decision to try the next option. The big win is not that backtracking magically removes exponential complexity; the win is that it lets you avoid wasted work by cutting off dead branches early and keeps your code readable.

In this article, we’ll learn what backtracking is, how to apply it using a Python code implementation, and understand its complexity.

What is Backtracking?

Backtracking is a depth-first search (DFS) over a space of choices. You maintain a partial solution (often called a path) and try to extend it step by step. At each step, you:

  1. Choose an option
  2. Explore deeper (recursive DFS)
  3. Undo the choice (revert state)
  4. Repeat with the next option

A clean way to summarize it is:

backtracking = dfs (explore deeper) + state (path/board) + undo (revert) + pruning (stop early when invalid).

This pattern shows up whenever the output is not a single number but a set of valid constructions. These constructions can be a list (subset/permutation), a string (parentheses), or a grid assignment (Sudoku).

Understanding Backtracking using a real-life example

Imagine you’re trying to guess a PIN, but you get feedback after each digit. You pick the first digit, and the system immediately tells you whether any valid PIN can start with that prefix. If you try 7 and it says, “No valid code starts with 7,” you stop right there and try a different first digit. You don’t keep guessing the remaining digits because once the prefix is invalid, every longer guess built on it is guaranteed to fail.

That’s the essence of pruning. Backtracking is built on the idea that partial progress is informative: a partially built solution can already tell you whether you’re on a viable path. If the current partial solution is impossible, you cut off that branch early instead of wasting time exploring it further.

When should you use Backtracking?

Backtracking should come to mind when the problem asks you to build or enumerate solutions, especially under constraints. The table below helps you out in this regard:

Problem signalWhat it usually meansHow backtracking helps
“Generate all …”You must enumerate a large output setThe recursion tree matches the output structure
“Find all valid …”Validity depends on rules/constraintsPrune invalid partial candidates early
“Choose k out of n …”Combination search with fixed depthDepth-limited recursion keeps it clean
“Return any one solution …”You only need to find one feasible constructionEarly exit when you find a valid leaf
Puzzle/grid constraintsEach assignments affect future choicesEarly constraint checks cut off massive parts of the search space

The key idea behind backtracking is simple:

Build a solution incrementally, and undo decisions to explore alternatives without rewriting state from scratch.

Backtracking structure

Backtracking feels “hard” only when the recursion is treated as magic and it becomes predictable once you define the search in plain terms before you write any code. The fastest way to do that is to answer five questions up front, these determine your recursion, your loop, and your undo logic.

  • What am I building? The partial solution you’re extending (a path list, a string under construction, or a grid/board assignment).
  • What can I try next? The candidate choices available from the current state (next number, next character, next cell, next direction).
  • What makes a choice illegal? The constraints that must always remain true (already used, violates sum/length, conflicts on row/col/diagonal, revisits a cell).
  • When do I record an answer? The goal condition that defines a complete solution. Some problems record only at the leaf (permutations), while others record at every node (subsets).
  • What must I revert before trying the next choice? Every state change you made while exploring a branch, i.e., path.pop(), unmark used/visited, restore a board cell, remove from a set/map, roll back a running sum.

Once these are explicit, most backtracking problems reduce to the same shape: loop over choices \rightarrow→ validate \rightarrow→ choose \rightarrow→ recurse \rightarrow→ then undo. The differences across problems are mostly just what the state is and how you prune.

Pruning while backtracking

Backtracking becomes interview-grade when you prune early. Pruning means detecting impossibility at a partial state, not at the end.

Examples of pruning checks:

  • Generate Parentheses: Never allow close > open, and never allow open > n
  • N-Queens: Never place a queen if it conflicts in column/diagonal
  • Combination Sum: stop if current sum exceeds target
  • Word Search: Stop if the next character doesn’t match or cell is already visited

The earlier you can prove “this branch can’t succeed,” the more of the tree you avoid exploring.

Backtracking template in Python

Many candidates remember the shape of backtracking code but struggle on two critical details:

  1. Where to record answers
  2. How to undo

A stable template keeps those steps consistent, so your code stays correct under pressure.

Two details matter more than they look:

  1. You typically append a copy of path, not the same list reference.
  2. The undo step must perfectly restore the state for the next iteration.

Example 1: Subsets (the “record at every node” version)

Subsets are an ideal first backtracking problem because every partial subset is always valid there’s nothing to prune. This makes it perfect for learning the core mechanics, i.e., exploring choices with recursion, using a start index to avoid duplicates, and undoing changes cleanly between branches.

Explanation of the algorithm

What’s happening structurally is important. Each recursion call represents “I am deciding the next element starting from index start.” The loop at line 9 tries all possible next picks, and start ensures you never go backward, so you never produce the same subset in multiple orders.

The following illustration for a recursion tree shows exactly how the code builds subsets, i.e., each node is the current path, and every edge is a “choose next element, recurse, then pop to backtrack” step.

Example 2: Permutations (introducing used as a constraint)

Permutations feel similar to subsets because you still build a path step-by-step, but the constraint is completely different. In subsets, you move forward with a start index to avoid reusing earlier positions. In permutations, there is no “start” because at each step you’re allowed to pick any remaining number. The only rule is that each element can appear once in the current arrangement.

That’s why we keep a used array. It marks which elements have already been used in the current permutation (path). Each recursive call tries all unused elements, and after exploring one choice, we undo it so the next choice starts from a clean state.

Complexity analysis

Backtracking often looks “expensive” because it explores many possibilities, and in many of these problems the number of valid solutions is already huge. If a problem can output (2^n) subsets or (n!) permutations, no algorithm can be faster than the time needed to generate that many results. In other words, exponential runtime here isn’t a mistake it’s often the minimum cost of enumeration.

Typical growth patterns you’ll see:

  • Subsets: (2^n)
  • Permutations: (n!)
  • Combinations (choose (k) from (n)): (nk)\binom{n}{k}(kn​)

Space usage is usually manageable because you only store the current recursion path:

  • Recursion stack: O(depth) often O(n) or O(k)
  • Helper state: typically O(n) e.g., used, visited, sets)

What interviewers want is not “make it polynomial,” but: explain why the search space is inherently large, then show that your pruning cuts off doomed branches early, so you’re not doing obviously wasted work.

Backtracking vs. related patterns

Backtracking also benefits from a quick “don’t misuse it” comparison. The table below compares backtracking with some other commonly used coding patterns:

AspectBacktrackingDFS traversal (general)Dynamic Programming
What you’re doingExploring choices, building solutionsTraversing a structureReusing overlapping subproblems
Core featureChoose → recurse → undoVisit nodes / edgesMemoization / tabulation
Typical outputAll / any valid constructionsReachability / orderingBest value / count
PruningCommon and essentialSometimesNot usually “pruning”; more reuse

Common mistakes when using Backtracking

Backtracking is conceptually simple, but state mistakes are common:

  • Forgetting to undo (pop(), unmark used, restore board cell)
  • Appending path without copying (result.append(path) creates aliasing bugs)
  • Recording answers at the wrong time (too early or only at the leaf when partials should count)
  • Not pruning early (checking constraints only at the end)
  • Duplicate results when input contains duplicates (needs sorting + “skip duplicates at same depth” rule)

Frequently Asked Questions

How do I know when to record an answer?

Record when you reach the goal condition. For permutations, that’s when len(path) == n. For subsets, every partial path is valid, so you record at every node.

How do I avoid duplicates when numbers repeat?

Sort the input and skip equal values at the same recursion depth. This prevents generating the same branch multiple times.

What if recursion depth is large?

In interviews, recursion is fine for typical constraints. In production, you can convert to an explicit stack if depth risks exceeding recursion limits.

Share with others:

Unlock up to 68% off lifetime access to Coding Interview prep with Educative

Getting ready for coding interviews or sharpening your problem-solving skills? Unlock a lifetime discount with comprehensive resources designed to help you master technical interviews.

Data structures and algorithms

Pattern-based problem solving

Mock interview practice

Real-world coding challenges

Coding Interview Logo