Backtracking: Subsets, Permutations & Combinations
Backtracking builds a solution one choice at a time and abandons a partial solution the moment it cannot be completed, exploring a state-space tree by depth-first search. We meet the universal choose/explore/un-choose template, derive the canonical enumerations — subsets (), permutations ($n!
╌╌╌╌
Dynamic programming, which closed the previous module, works when a problem has overlapping subproblems we can tabulate. But a great many problems ask us instead to enumerate or search a combinatorial space: list every subset, every permutation, every way to place eight queens, every assignment satisfying a formula. These spaces are exponential, so we cannot afford to materialize them, yet we can traverse them selectively. Backtracking is the disciplined depth-first walk of such a space: it builds a candidate solution one decision at a time and abandons a partial candidate the instant it proves it cannot be extended to a valid complete one.1 This act of abandonment, the backtrack, is what separates a directed search from blind brute force.
This lesson opens the module by establishing the paradigm and instantiating it on the three enumerations every later technique builds on (subsets, permutations, and combinations) together with the two ideas that make backtracking fast in practice: deduplication of equal choices and pruning of infeasible subtrees.
The paradigm: choose, explore, un-choose
Think of a solution as a sequence of decisions. At each step we have a partial solution and a set of valid choices to extend it. We pick one choice, recurse to extend further, and then undo that choice before trying the next one. The recursion thus traverses a state-space tree (also called a decision tree or choice tree): the root is the empty partial solution, each edge is one choice, each node is the partial solution accumulated so far, and the leaves are complete candidates.2 The walk is depth-first.
The un-choose step is what lets a single mutable buffer serve the entire tree: by the time control returns from a child, the buffer is byte-for-byte what it was before we descended, so the next sibling starts from a clean slate.
- 1if then
- 2record a copy of
- 3return
- 4for each do
- 5if then
- 6continueprune
- 7choose
- 8explore
- 9un-choose
Everything in this lesson is a specialization of this skeleton: the four primitives , , , and the choose/undo pair are all we ever change.
Subsets: the power set in
The cleanest decision tree is the power set of . Each element faces one binary decision, in the subset or out, so the tree is a perfect binary tree of depth , and its leaves enumerate the subsets. This is the include–exclude recursion:
- 1if then
- 2record a copy of
- 3return
- 4exclude
- 5include
- 6explore
- 7un-choose
An equivalent and often handier formulation uses a start index so that every node, not only the leaves, is a valid subset. We loop over choices , and each recursion only ever looks forward from the chosen index, which guarantees we generate each subset once, in lexicographic order of indices:
- 1record a copy ofevery node is a subset
- 2for to do
- 3choose
- 4forward only
- 5un-choose
Advancing the start index to in the recursive call is what stops the same subset from being generated twice, once as -then- and once as -then-: an element earlier in the array is never chosen after a later one. Both formulations do recursive calls and spend to copy each emitted subset, for total, unavoidable, since the output itself has that size.
Permutations: orderings
A permutation uses every element, so completeness is all chosen,
and the
valid choices at each step are the elements not yet used. We track membership
with a boolean used[] array; the branching factor shrinks from at the root
to , then , giving exactly leaves.
- 1if then
- 2record a copy of
- 3return
- 4for to do
- 5if then continuenot a valid choice
- 6;choose
- 7explore
- 8;un-choose
An alternative avoids the auxiliary array by swapping in place: to permute , swap each candidate into position , recurse on , then swap it back: the swap-back is the un-choose. Either way the work is , dominated by emitting permutations of length .
Each root-to-leaf path consumes every element exactly once; the accented path (root ) builds the permutation . The fan-out is at the root, at depth one, and at depth two, so the leaf count is the falling product .
Combinations: with a start index
A combination is a subset of a fixed size where order does not matter.
We want but not also , so we reuse the start-index trick
from subsets, which generates each combination exactly once by only ever choosing
forward. Completeness is now we have collected elements.
- 1if then
- 2record a copy of
- 3return
- 4for to do
- 5choose
- 6forward only
- 7un-choose
The start index does the combinatorial bookkeeping: because the chosen indices strictly increase along any root-to-leaf path, each -subset corresponds to exactly one path, and we enumerate all of them with no duplicates. This is the same idea as the start-index subset enumerator: a combination is simply a subset enumeration cut off at depth .
Handling duplicates: skip equal siblings
When the input multiset contains repeated values, say , the naive enumerator emits the same combination twice, because the two s are distinguishable by position but identical in value. The standard fix is to sort the array, then at each level skip a choice equal to the one just tried at the same depth.
The condition carries the logic. The first occurrence of a value at a given level (when ) is always allowed; we only skip subsequent equal values at the same depth.
This is a soundness claim about the prune: a pruning rule is sound when it never discards a branch that contains a solution we have not already found elsewhere, so a sound prune keeps the search complete — every distinct solution is still reported.
The rule skips equal siblings, not equal ancestors: choosing then descending and choosing is legitimate (it uses both copies), and there so the guard does not fire.
At the root, repeats sibling (here ), so its whole subtree is pruned as a duplicate. Inside the branch the same guard fires, pruning . But descending from to is ancestor reuse, not a sibling repeat — there , the guard does not fire, and both copies of are legitimately used.
This same machinery distinguishes two classic problems. In Combination Sum,
each number may be reused unboundedly, so after choosing we recurse with
start = i (do not advance); staying on the same element keeps it available.
In Combination Sum II, each input number may be used at most once and
duplicates exist, so we recurse with start = i+1 (advance) and apply the
equal-sibling skip above to avoid duplicate combinations.
Pruning: kill infeasible subtrees early
Everything so far enumerates all of a space. Backtracking becomes useful when the problem constrains the answer, because then we can prune: refuse to descend into a subtree the moment we can prove it contains no solution. Pruning prunes whole subtrees, so a single early cut can save exponentially many leaves. A prune is sound precisely when it only ever cuts subtrees that provably hold no solution; both cuts below qualify, so the search stays complete — no real solution is lost.
Take Combination Sum with a target over sorted positive numbers. Two
prunes apply at the point we consider extending partial (current sum ) by
:
- Over-target cut. If , this choice overshoots; and since the
array is sorted ascending, every later overshoots too, so we
breakout of the entire loop, not merelycontinue. - Reachability cut (a general lower-bound prune). If even the smallest remaining additions cannot reach , abandon the branch.
Each branch reaching (the dashed nodes) is dropped without
descending further; the blue path is the live solution
. Pruning determines the practical cost: a search that looks exponential can
run in milliseconds when the prune severs the bulk of the tree, while a poorly
pruned search of the same size is hopeless. This is why Skiena frames practical
combinatorial search as the art of pruning.
2
To see how much the prune saves, trace Combination Sum with target on
the sorted candidates , each reusable. The recursion carries the
running sum and a start index; the over-target cut breaks the loop the moment
(safe because later candidates are only larger).
| Path so far | tries | outcome | |
|---|---|---|---|
| 0 | descend | ||
| 2 | descend | ||
| 4 | descend | ||
| 6 | break (also skipped) | ||
| 7 | — | record | |
| 5 | break | ||
| 8 | overshoot at entry | pruned | |
| 3 | descend | ||
| 6 | break | ||
| 7 | — | record |
The two solutions and are found while the over-target break
lops off every branch that would push past — the subtrees under
, , , and the whole region never materialize.
Without the sorted-break prune the same search would blindly expand all of them
before discovering they overshoot.
Complexity: the size of the explored tree
There is no single formula for backtracking complexity
; the running time is
simply the size of the state-space tree we actually explore, times the work
per node. For a full enumeration this is the output size:
for subsets, for permutations, for
combinations. More generally the cost is , and for problems whose answers we emit, it is bounded below by
: we cannot beat the cost
of writing the output.
The lever is always the same: a tighter predicate cuts subtrees nearer the root, and the savings compound exponentially with the depth of the cut.
From a 1965 method to modern industrial solvers
The choose/explore/un-choose skeleton is the direct ancestor of some of the most widely used software in computing.
Naming the method. The recursive-abandonment idea is old — the term
backtrack was coined by D. H. Lehmer in the 1950s — but Golomb and Baumert's 1965
paper Backtrack Programming
was the first systematic treatment, and it already
identified the two levers this lesson emphasizes: preclusion (pruning) and
choosing which variable to branch on next.4 Every refinement since is a
sharper answer to those two questions.
Dancing Links. For exact-cover problems (Sudoku, pentomino tiling, -queens), Knuth's Algorithm X with the Dancing Links data structure (2000) makes the choose/un-choose pair almost free: the constraint matrix is a doubly-linked mesh, covering a column unlinks its rows in , and un-covering relinks them by running the same pointer operations backward — the un-choose step made literal.5 It remains the fastest general exact-cover solver and the reason a Sudoku solves in microseconds.
SAT solvers. Boolean satisfiability is backtracking's largest application. The DPLL algorithm (Davis, Putnam, Logemann, Loveland, 1962) is backtracking over truth assignments with unit propagation — a constraint-propagation prune. Its modern descendant, CDCL (conflict-driven clause learning), adds two ideas that transform the search: when a branch fails, it analyzes the conflict to learn a new clause that prunes many future branches, and it non-chronologically backjumps past irrelevant decisions rather than backtracking one level at a time.6 CDCL solvers routinely dispatch formulas with millions of variables, and are used in hardware verification, program analysis, and automated planning — all built on the recursion of this lesson.
Takeaways
- Backtracking is depth-first search of a state-space tree: build a partial solution incrementally, and abandon any branch that cannot reach a valid complete solution.
- The universal template is choose / explore / un-choose: a single mutable buffer suffices because the undo step restores it before each sibling is tried.
- Subsets () come from an include/exclude binary recursion or a
forward-only start index; permutations () from a
used[]array or in-place swapping; combinations () from a start index that forces increasing choices so each set is generated once. - Duplicates are handled by sorting and skipping equal siblings at the same
depth (
if i > start and a[i] == a[i-1] continue), which drops identical subtrees without losing distinct solutions; reuse-allowed variants keepstart = iinstead of advancing. - Pruning, cutting infeasible or non-improving subtrees early (sum exceeds target, bound can't beat the best), is what separates exponential-but-fast from hopeless; one early cut saves an exponential subtree.
- Running time is the size of the explored tree times per-node work: exponential in the worst case, but tamed to practicality by good pruning on typical inputs.
Footnotes
- Erickson, Ch. — Backtracking: incrementally constructing a solution and abandoning partial candidates that cannot be completed. ↩
- Skiena, §7 — Combinatorial Search and Heuristic Methods: the state-space search tree and pruning as the core of efficient exhaustive search. ↩ ↩2
- CLRS, Ch. — Exhaustive Search: backtracking explores only feasible extensions, exponential in the worst case but far smaller in practice. ↩
- Golomb, S. W. & Baumert, L. D. (1965),
Backtrack programming,
Journal of the ACM 12(4), 516–524 — the first systematic study of backtracking, naming preclusion (pruning) and variable ordering as the two levers of efficiency. ↩ - Knuth, D. E. (2000),
Dancing links,
in Millennium Perspectives in Computer Science, 187–214 — Algorithm X with the doubly-linked Dancing Links structure making cover/uncover (choose/un-choose) and reversible. ↩ - Marques-Silva, J. P. & Sakallah, K. A. (1999),
GRASP: a search algorithm for propositional satisfiability,
IEEE Transactions on Computers 48(5), 506–521 — conflict-driven clause learning and non-chronological backjumping, extending the DPLL backtracking search (Davis, Logemann & Loveland, 1962) into modern industrial SAT solvers. ↩
╌╌ END ╌╌