Chapter 5: Backtracking
Explore systematic search through decision spaces using backtracking. Learn how to enumerate subsets, combinations, and permutations by building partial solutions incrementally, pruning invalid branches early, and applying memoization when subproblems overlap.
- Topics
- 8
- Demos
- 5
- Videos
- 3
- Min read
- 34
Chapter Overview
Backtracking is a refined form of brute-force search. Instead of generating every possible solution and then checking validity, backtracking builds solutions incrementally---placing one element at a time---and abandons a partial solution as soon as it determines that it cannot possibly lead to a valid answer.
The key idea is a state-space tree: each node represents a partial solution, each edge represents a decision (include an element, pick a digit, place a queen), and each leaf is a complete candidate. DFS traverses this tree, and pruning cuts off entire subtrees when constraints are violated, dramatically reducing the search space.
Backtracking problems share a common template: choose a candidate, recurse with the updated state, then undo the choice (backtrack) before trying the next candidate. Mastering this template unlocks an entire class of interview problems---from generating subsets and permutations to solving constraint-satisfaction puzzles like N-queens and Sudoku.
This chapter covers:
- State-space trees: Visualizing the decision tree and understanding how DFS explores it
- Combinatorial generation: Subsets, combinations, and permutations using systematic enumeration
- Pruning strategies: Cutting invalid branches early to avoid unnecessary work
- Classic backtracking problems: Phone letter combinations, valid parentheses, and word break
- Deduplication: Handling duplicate elements without producing duplicate results
Chapter Roadmap
Click any topic to jump in
DFS with States
Every backtracking algorithm is a DFS over a state-space tree — nodes are partial solutions, edges are choices.
Combinations and permutations are the two fundamental shapes that map to the DFS template.
Combinations & Permutations
The two canonical shapes: choose $k$ from $n$ (order-free) and order $n$ items (order matters).
Pruning
Cut branches that cannot satisfy constraints — turns exponential search spaces into tractable ones.
Letter Combinations
Generate the Cartesian product of digit-to-letter sets — a clean application of the combinatorial template.
Generate Parentheses
Count of valid strings is the $n$-th Catalan number; the backtrack tree grows with constraint-guided pruning.
Each classic problem picks a different strategy — unrestricted product, constraint-driven, or choice-tracking.
Permutation Strategies
Two equivalent approaches — visited-array tracking versus in-place swap — with the same $\Theta(n \cdot n!)$ cost.
Memoization
When the backtrack tree has overlapping subproblems, caching collapses exponential work to polynomial.
Deduplication
Sort and skip — the standard way to avoid generating the same subset or permutation twice.
Topics
8 topics in this chapter
A pizza shop offers three toppings, and you want to list every possible pizza, from plain cheese to all three toppings together. With three toppings you can write the eight options by hand, but with twenty toppings there are over a million, and you need a systematic way to produce each one exactly once without missing any.
The previous chapter, Depth First Search & Trees, walked real trees made of node objects. Backtracking walks a tree that is never stored: a state-space tree, where the root is an empty choice, each level decides one thing, such as whether topping i is on the pizza, and each root-to-leaf path is one complete candidate. The same DFS you used on binary trees explores it, carrying a partial solution down and undoing the last decision on the way back up.
We first draw the state-space tree for subsets and count its nodes and leaves, which explains why this kind of search is exponential. Then we distill the choose, explore, unchoose template that every later topic in this chapter reuses, and look at the two bugs that break it most often.
Definition
Backtracking is a depth-first search over a state-space tree whose nodes are partial solutions and whose edges extend a partial solution by one choice. At each node it chooses an option, recurses, and then undoes the choice before trying the next option, so a single mutable state is reused across the whole search. Leaves, or other accepted nodes, are recorded as solutions.
In this topic
- 1State-Space Tree
- 2Backtracking Template
State-Space Tree
A state-space tree is an abstract tree where each path from root to leaf represents a sequence of decisions that builds one complete candidate solution. The root is the empty state (no decisions made). At each level, you decide what to do with the next element---include it, exclude it, or choose from a set of options. DFS explores this tree, visiting every path. The total number of leaves equals the total number of candidate solutions.
A backtracking algorithm is a DFS over a state-space tree whose nodes are partial solutions and whose edges represent extending a partial solution by one choice. If each node has at most children (branching factor) and depth is at most , the tree has at most leaves and nodes. Work per node is at least to copy the state, so total time is — inherently exponential without pruning.
Visualize the state-space tree for generating all subsets of [1, 2, 3].
Backtracking Template
Building a fresh copy of the partial solution at every node wastes time and memory, so backtracking keeps one shared list and edits it in place. Each node repeats three steps. Choose: append a candidate to the shared list. Explore: recurse, so the child sees the extended state. Unchoose: pop the candidate, restoring exactly the state the parent had, before trying the next option. This is the down-and-up flow from DFS on Trees, with state passed down by mutation. Two bugs dominate. Forgetting to unchoose leaks one branch's choices into its siblings, and recording the list without copying it stores a reference that later edits overwrite.
The template is: if state is a solution, record it and return; for each choice, apply it (mutate state), recurse, undo it (restore state). Undoing is what makes 'backtracking' — it reuses one mutable buffer across the entire traversal for extra memory. The total time remains but the constants are smaller than building new states.
Generate all subsets of a list using the backtracking template.
Related Problems on PixelBank
A coach must pick 2 players out of 4 for a doubles match, and separately decide the batting order of 3 players. The first question does not care about order, since Ann with Bo is the same pair as Bo with Ann, while the second question does, since Ann batting first differs from Ann batting last. Mixing them up either lists the same pair twice or misses half the orders.
The previous topic, DFS with States, gave the choose, explore, unchoose template and applied it to subsets. Combinations and permutations are the two shapes almost every enumeration problem reduces to, and each needs one small change to that template. Combinations only choose elements at indices after the last chosen one, so each group is produced in exactly one order. Permutations may choose any element not yet used, so they track usage with a boolean array.
We first build combinations and count how many there are with the binomial coefficient, adding a simple pruning rule that skips branches too short to finish. Then we build permutations, count them with the factorial, and compare the sizes of the two search trees.
Definition
A k-combination of n items is an unordered selection of k distinct items, and there are C(n, k) = n! / (k! (n-k)!) of them. A permutation of n items is an ordered arrangement of all of them, and there are n! of them. Backtracking generates combinations by choosing indices in increasing order, and permutations by choosing any unused item at each position.
In this topic
- 1Combinations (Choose k from n)
- 2Permutations (All Orderings)
Combinations (Choose k from n)
A combination selects k elements from n without regard to order. There are C(n, k) = n! / (k!(n-k)!) such selections. The backtracking approach iterates from a start index forward, ensuring elements are chosen in increasing order so that {1,2,3} is generated but {3,1,2} is not. Once the current selection reaches size k, it is recorded and the recursion stops. Pruning is possible: if fewer than k - len(current) elements remain, that branch cannot produce a valid combination and should be skipped.
The number of -combinations from items is . The recursive identity maps directly to 'include or exclude the current item', yielding a backtracking tree with exactly leaves. Each leaf does work to copy, so total time is .
Generate all combinations of 2 elements from [1, 2, 3, 4].
Permutations (All Orderings)
In a permutation, position matters, so [1, 2, 3] and [3, 2, 1] are different answers, and the forward-only start index used for combinations would miss most of them. Instead, every position may take any element that has not been placed yet. A boolean array used, where used[i] is True while nums[i] sits in the current arrangement, answers that question in O(1). At depth d there are n - d choices, so the tree has n! leaves. The unchoose step must reset both the list and the flag. Forgetting the flag leaves the element marked as used forever, so every later branch skips it and loses permutations.
There are permutations of distinct items. Backtracking places one item per level: level 0 has choices, level 1 has , etc., giving leaves. Copying each into the output costs , so total time is . This is asymptotically tight because the output itself has that size.
Generate all permutations of [1, 2, 3].
Related Problems on PixelBank
A delivery van can carry 10 kilograms, and you want every combination of parcels that fills it exactly. Listing all subsets of 30 parcels means about a billion candidates, yet most are hopeless early: once the parcels already chosen weigh 11 kilograms, adding more cannot bring the total back to 10. Checking that and stopping at once removes every subset built on top of that choice in a single step.
The previous topic, Combinatorial Search, enumerated every combination and permutation, and the trees grew as C(n, k) and n!. Pruning is the technique that keeps backtracking practical: before extending a partial solution, test whether it can still lead to a valid answer, and if not, return without exploring the subtree below it. A check that costs O(1) can remove millions of nodes, and ordering the input, for example sorting it, often makes the check sharper.
We first apply early termination to subset sum, where sorting lets one failed check end a whole loop. Then we solve N-queens, the classic constraint problem, using three sets that make every column and diagonal test O(1), and measure how many nodes the pruning removes.
Definition
Pruning is the practice, during backtracking, of abandoning a partial solution as soon as a check shows that no completion of it can satisfy the constraints. The entire subtree below that node is skipped. Pruning never changes the set of solutions found, only the number of nodes visited, provided the check never rejects a partial solution that could still succeed.
In this topic
- 1Early Termination and Constraint Checking
- 2N-Queens Constraint Propagation
Early Termination and Constraint Checking
Early termination means stopping recursion the moment a constraint is violated, rather than continuing to build an invalid solution. At each step, you check whether the current partial solution can still become valid. If not, you return immediately without exploring deeper. The earlier you detect invalidity, the more subtrees you skip. Common constraints include: value ranges, uniqueness, adjacency rules, and capacity limits. Effective pruning reduces exponential search spaces to manageable sizes.
Pruning discards any subtree whose partial solution already violates a constraint. If a constraint on partial state can be checked in and eliminates an entire subtree of size , pruning saves work at cost — a huge win when . The canonical example is N-Queens: without pruning , with constraint checking about .
Find all subsets of [2, 5, 3, 8, 1] that sum to exactly 10. Use pruning to skip subsets whose partial sum already exceeds 10.
N-Queens Constraint Propagation
The N-queens problem places N queens on an N x N board such that no two share a row, column, or diagonal. Since each row must contain exactly one queen, we place one queen per row and track three sets: occupied columns, occupied main diagonals (row - col is constant), and occupied anti-diagonals (row + col is constant). Before placing a queen, we check all three sets. If any is occupied, we prune that branch. This reduces the search from N^N to roughly N! possibilities, with further pruning cutting it even more.
Placing one queen per row already shrinks the raw space from placements to the permutation tree, which for has nodes. Adding the two diagonal sets, and , cuts the search to 2,057 nodes for all 92 solutions. Each check is , so the total cost is proportional to the nodes visited: still exponential in , but small enough to solve boards up to about exhaustively.
Solve the 4-queens problem and print all valid board configurations.
Related Problems on PixelBank
Before smartphones had keyboards, each number key on a phone also carried letters: 2 for abc, 3 for def, up to 9 for wxyz. Businesses picked numbers that spelled words, and a caller who wanted to decode a number such as 23 had to consider every letter string it could represent. The count multiplies quickly: two digits give 9 strings, and four 7s or 9s give 256.
The previous topic, Pruning, cut branches that could not succeed. This problem is the opposite case: every branch succeeds, so there is nothing to prune, and the search tree is exactly as large as the answer. That makes it a clean exercise of the template. The recursion depth is the number of digits, and the branching factor at each level is the number of letters on that digit's key, three or four. The output is the Cartesian product of the letter groups.
We first write the recursive solution, one level per digit, and trace how its tree grows. Then we build the same product iteratively, level by level, and compare the memory each version uses, since the recursive version holds one path while the iterative version holds whole levels.
Definition
Given a string of digits from 2 to 9, the letter-combination problem asks for every string formed by choosing one letter from each digit's keypad group, in digit order. The answer is the Cartesian product of those groups. Its size is the product of the group sizes, at most 4^n for n digits, so any algorithm needs time at least proportional to n times that count.
In this topic
- 1Digit-to-Letter Mapping
- 2Iterative Cartesian Product Alternative
Digit-to-Letter Mapping
The classic phone keypad maps each digit 2-9 to a group of letters: 2->abc, 3->def, 4->ghi, 5->jkl, 6->mno, 7->pqrs, 8->tuv, 9->wxyz. Given a string of digits like "23", the task is to produce all strings formed by choosing one letter from each digit's group. The number of combinations is the product of the group sizes: for "23" that is 3 x 3 = 9 strings. This is a direct application of the backtracking template where each level of recursion processes one digit.
The phone mapping assigns or letters per digit. For an -digit input, the output has strings, bounded by . Backtracking has depth , branching factor up to , so the state-space tree has leaves and total work — dominated by the size of the answer.
Generate all letter combinations for the digit string "23".
Iterative Cartesian Product Alternative
The phone letter problem is mathematically a Cartesian product of the letter groups for each digit. While backtracking solves it recursively, you can also solve it iteratively: start with an empty string, and for each digit, append every possible letter to every existing partial result. This produces the same output and illustrates that backtracking and iterative expansion are two views of the same combinatorial process. Python's itertools.product captures this pattern directly.
The iterative equivalent starts with '' and, for each digit , replaces the current list with . This is exactly one level of the BFS version of the tree, producing the same strings with the same asymptotic cost but different memory profile — queue size versus stack.
Solve the phone letter problem iteratively using list expansion.
Related Problems on PixelBank
A code editor wants to offer every correctly nested arrangement of n pairs of brackets, for example to test a parser. With 3 pairs there are 6 characters and 2^6 = 64 strings of open and close brackets, but only 5 of them are well formed; strings such as ")(()()" close a bracket that was never opened. Generating all 64 and filtering out the bad ones wastes most of the work, and the waste grows quickly with n.
The previous topic, Letter Combinations of Phone, had no invalid branches, so its tree matched the answer. Here most branches are invalid, and the fix is the pruning idea from earlier in the chapter, applied through two counters. Track how many opening and closing brackets have been placed. An opening bracket is allowed while fewer than n have been used, and a closing bracket is allowed only while it would match an unclosed opening bracket. Every string built this way is valid, so no filtering is needed.
We first derive and trace the two counting rules. Then we count the results, which are the Catalan numbers, and use that count to check the generator's output.
Definition
A string of n opening and n closing parentheses is well formed if every prefix contains at least as many opening as closing parentheses. Backtracking generates exactly these strings by adding "(" while fewer than n are used and ")" while fewer closing than opening brackets are placed. There are C_n = C(2n, n) / (n + 1) such strings, the n-th Catalan number.
In this topic
- 1Open/Close Counting Constraint
- 2Catalan Numbers and Counting
Open/Close Counting Constraint
Valid parentheses strings satisfy two rules at every prefix: (1) the number of open parentheses is at most n, and (2) the number of close parentheses never exceeds the number of opens. These two rules serve as pruning conditions in the backtracking tree. If open_count < n, we can add '('. If close_count < open_count, we can add ')'. When both counts reach n, the string is complete and guaranteed to be valid. The total number of valid strings is the nth Catalan number: C(n) = (2n)! / ((n+1)! * n!).
A string of open and close parens is valid iff at every prefix, , and the totals are equal. Backtracking keeps counters: you can add '(' while and ')' while . This constraint prunes most of the possible strings down to the Catalan count — a huge saving.
Generate all valid parentheses strings for n = 3.
Catalan Numbers and Counting
The number of valid parenthesizations of n pairs is the nth Catalan number: C(n) = C(2n, n) / (n+1). The sequence starts 1, 1, 2, 5, 14, 42, 132, 429, ... Catalan numbers appear in many combinatorial problems: the number of binary search trees with n nodes, the number of ways to triangulate a polygon, and the number of paths on a grid that stay below the diagonal. Understanding this count helps you verify your backtracking solution produces the correct number of results.
The number of valid parenthesis strings of length is the -th Catalan number . Generating each takes to copy, so total time is . Catalan numbers also count balanced binary trees, triangulations of polygons, and Dyck paths — all isomorphic to parenthesis strings.
Verify that the parentheses generator produces C(n) results for n = 1 through 6.
Related Problems on PixelBank
A travelling salesperson with five cities to visit wants to try every route, a test harness wants to feed a function its arguments in every order, and a puzzle solver wants to try every arrangement of tiles. All three need the permutations of a list, and with n items there are n! of them: 120 for five, but over 3.6 million for ten, so the way they are generated affects memory and speed.
The previous topic, Generate Parentheses, pruned a binary choice at each step. Permutations, introduced in Combinatorial Search, have no invalid branches, so the interesting question is how to track what has already been placed. The visited-array method builds a separate output list and marks each element as used. The swap-based method builds the permutation inside the input array itself, by swapping an element into the next free position and swapping it back afterwards.
We first implement and trace the visited-array method, which produces lexicographic order on a sorted input. Then we implement the swap method, compare its output order and memory use, and look at what goes wrong if the swap back is forgotten, a mistake that silently produces duplicate permutations.
Definition
A permutation of n distinct items is an ordering of all of them, and there are n! permutations. Backtracking fills one position per level. The visited-array method picks any unused item and records its use in a boolean array; the swap method swaps each candidate into the current position of the array itself. Both run in Theta(n * n!) time, including copying the output.
In this topic
- 1Visited-Array Permutations
- 2Swap-Based In-Place Permutations
Visited-Array Permutations
The visited-array approach maintains a boolean array where used[i] indicates that nums[i] has already been placed in the current permutation. At each recursive level, we iterate over all elements and skip those already used. This produces permutations in lexicographic order when the input is sorted. The space overhead is O(n) for the used array plus O(n) for the current permutation list, plus O(n) recursion depth---total O(n) auxiliary space beyond the output.
A boolean array tracks which elements are already in the current permutation prefix. At depth , there are unused choices, giving the total number of explored states as . Each state does work to set/clear a flag, so per-permutation overhead is — identical to the swap method asymptotically.
Generate all permutations of ['a', 'b', 'c'] using the visited-array approach.
Swap-Based In-Place Permutations
The swap-based approach fixes one position at a time by swapping elements. To fill position first, swap each element from positions first through n-1 into position first, recurse to fill position first+1, then swap back. This uses O(1) extra space (beyond the recursion stack) because the permutation is built in place within the input array. The trade-off is that permutations are not generated in lexicographic order, and handling duplicates requires extra care.
Instead of tracking visited elements, swap element with position to 'select' it. This uses extra space beyond the mutable array and the call stack. Total time matches the visited-array method. Downside: the permutations come out in a different order, and if ordering matters (sorted output) you must collect and sort, losing the in-place benefit.
Generate all permutations of [1, 2, 3] using the swap method.
Related Problems on PixelBank
A search engine receives the query "thisisapen" with the spaces missing and must decide whether it splits into dictionary words. Backtracking can try every first word and recurse on the rest. The trouble is that different first choices often leave the same remaining text, and a plain backtracking search re-solves that remainder from scratch every time it arrives there.
The previous topic, Permutations, had a tree in which every path led to a different state, so nothing could be reused. Word Break is different: its state is just the index where the unsegmented suffix begins, so there are only n + 1 distinct states, however many paths lead to them. Memoization records the answer for each state the first time it is computed and returns the stored answer on every later visit, the same fix that collapsed the Fibonacci call tree in the previous chapter.
We first measure the overlap: on twenty a's followed by a b, the naive search makes over 260,000 calls. Then we add a memo keyed on the start index, cut the count to 58, and derive the polynomial running time. This topic is the bridge from backtracking to dynamic programming.
Definition
Memoization caches the result of a recursive function for each distinct argument, so each subproblem is solved at most once. It helps whenever different paths of a search reach identical states, which is called overlapping subproblems. In Word Break, the state is the start index of the remaining suffix, so there are n + 1 states, and the memoized search runs in polynomial time.
In this topic
- 1Overlapping Subproblems in Backtracking
- 2Memoized Word Break
Overlapping Subproblems in Backtracking
Overlapping subproblems occur when the same recursive subproblem is encountered multiple times during exploration. In standard backtracking, each call is independent because the state changes at each step. But in problems like Word Break, the remaining string after different valid splits can be identical. Without memoization, the same suffix is re-examined from scratch, leading to exponential time. Memoization turns this into a polynomial-time solution by ensuring each unique suffix is solved only once.
A backtracking tree has overlapping subproblems iff distinct paths from the root lead to identical states. For word break, the state is the current suffix — different split histories can reach the same suffix, so naive backtracking is . Identifying overlap is the prerequisite for memoization; without overlap, memoization cannot help.
Show how Word Break without memoization repeats work on the string 'catsanddog' with dictionary ['cat', 'cats', 'and', 'sand', 'dog'].
Memoized Word Break
By adding a memo dictionary that maps each starting index to its result (True or False), we ensure each index is processed at most once. The first time we reach index i, we compute whether s[i:] can be segmented and store the result. Future calls with the same index return immediately. This reduces the time complexity from O(2^n) to O(n^2 * m) where n is the string length and m is the maximum word length in the dictionary.
Cache results on index (or equivalently, the suffix). There are only distinct suffixes, so once memoized, each subproblem is solved once. With states and per state (scanning word lengths up to ), total time is — a polynomial vs. exponential collapse. This pattern — memoizing over start index — recurs throughout DP on strings.
Solve Word Break with memoization and compare the call count to the naive version.
Related Problems on PixelBank
A shop's basket contains two identical apples and one banana, and you want every different selection of fruit. The subset generator from DFS with States treats the two apples as separate items, so "one apple" appears twice in its output, once for each apple. Removing duplicates afterwards with a set works, but the search still does all the duplicate work, and with many repeated items that work grows exponentially.
The previous topic, Memoization & Word Break, avoided repeated work by caching states. Deduplication avoids it by never generating a duplicate in the first place. The standard technique has two parts. Sort the input so equal values sit next to each other. Then, when choosing among candidates at one level of the tree, skip a value equal to the one just tried at that same level, because choosing it would rebuild a subtree that already exists. Equal copies are always used in their left-to-right order.
We first apply sort and skip to subsets with duplicates and trace which branches it removes. Then we adapt the rule to permutations, where the condition checks the used array instead of the start index, and compare the node counts with and without it.
Definition
Deduplication in backtracking produces each distinct result exactly once when the input contains repeated values. Sort the input so equal values are adjacent, then skip a candidate that equals the previous candidate at the same tree level. For subsets the test is i > start and nums[i] == nums[i-1]; for permutations it is nums[i] == nums[i-1] and not used[i-1].
In this topic
- 1Sort and Skip Pattern
- 2Permutations with Duplicates
Sort and Skip Pattern
The sort + skip deduplication pattern works as follows: (1) Sort the input array so duplicates are adjacent. (2) At each level of the backtracking tree, when iterating over candidates, skip nums[i] if it equals nums[i-1] and i-1 was not selected at this level. The key insight is that if two identical elements could both be chosen, we enforce that the earlier one must be chosen first. This prevents generating the same subset or combination through different orderings of identical elements. The condition varies slightly between subsets (skip if i > start and nums[i] == nums[i-1]) and permutations (skip if nums[i] == nums[i-1] and not used[i-1]).
To avoid generating the same subset/permutation twice when the input has duplicates, sort first so equal elements are adjacent. Then, at each choice point, skip the current element if it equals the previous and the previous was not used in this branch. Sorting is upfront; the skip rule keeps the output unique without using a hash set, preserving the asymptotic cost of the raw backtracking.
Generate all unique subsets of [1, 2, 2] without duplicates.
Permutations with Duplicates
For permutations with duplicates, the skip condition is slightly different: skip nums[i] if nums[i] == nums[i-1] and used[i-1] is False. The reasoning: if two elements are identical, we require the first one to be placed before the second. If the first copy is not yet used (used[i-1] is False), then placing the second copy would create a duplicate permutation---the same arrangement was already generated when the first copy was placed at this position. Sorting ensures duplicates are adjacent so this check works correctly.
For a multiset with counts , the number of distinct permutations is — the multinomial coefficient. The sort-and-skip trick ensures backtracking generates exactly this count, with no duplicates produced and discarded. Without skipping, you would produce and dedupe afterwards — the same answer at much higher cost.
Generate all unique permutations of [1, 1, 2] without duplicates.