Backtracking & Design
The last six drills. Three generate every valid answer by walking a decision tree and undoing each choice on the way back up; three build small data structures whose every operation must run in O(1) or O(log k). Both halves reward the same habit: state an invariant first, then write the code that preserves it.
Search Trees and Invariant-Driven Design
Backtracking is depth-first search over a tree of partial solutions. Each node is a partial answer (a prefix of a permutation, a partial sum, a half-built string); each edge is one choice. The algorithm walks down by choosing, records a result at a leaf, and walks back up by un-choosing โ restoring the shared state exactly as it was so the next sibling starts clean. Because one mutable path is reused for the whole search, memory is only O(depth) even when the tree has n! leaves. The running time is bounded by the size of the tree, which is why pruning โ refusing to enter a branch that cannot lead to a valid answer โ is the main optimisation.
The second half of the page is data-structure design: Min Stack, LRU Cache and a top-k heap. These are not search problems, but they share the mindset. You decide what invariant the structure maintains ("every entry knows the minimum beneath it", "the list is ordered by recency", "the heap holds the k best so far"), and every operation does a constant amount of work to keep it true. When a single built-in structure cannot give O(1) for all operations, you compose two of them.
- Leaves ร cost per leaf: permutations have n! leaves and copying each costs O(n) โ O(n ยท n!).
- Branchingdepth: a combination search with branching b and depth d is O(bd) before pruning.
- Output-sensitive bounds: Generate Parentheses produces Catalan(n) โ 4n/(n1.5โฯ) strings โ you cannot beat the output size.
Which Tool Fits the Question
Use the flow below to pick the technique from the problem statement. The first three rows are search problems that differ only in how the next choices are generated; the last three are design problems that differ in the invariant being maintained.
Four Reusable Templates
Four templates: two for search (how to enumerate choices without duplicates) and two for design (how to make every operation cheap).
choices, valid and isComplete change between problems.start onward, so the same multiset is never produced in two orders. Sorting first lets a single break discard every remaining candidate.Permutations โ Ordered Choices
nums of distinct integers, return all possible permutations, in any order.boolean[] used answers "is this number already placed?" in O(1), and un-marking it on the way back up frees it for the sibling branches.
There is no way around generating n! outputs, so the goal is to avoid extra waste: no duplicate permutations, no re-scanning the path to see what is used, and no new list per call. The original drill used the in-place swap variant (swap nums[start] with each nums[i], recurse on start+1, swap back). It is also correct and needs no used array, but its output order is not lexicographic โ its comment claimed the last two results were [3,1,2], [3,2,1] while it actually produces [3,2,1], [3,1,2]. The used[] version below emits permutations in lexicographic order for sorted input and generalises to Permutations II (duplicates).
| Approach | Time | Space | Notes |
|---|---|---|---|
| Generate all nn sequences, filter | O(nn ยท n) | O(n) | Wasteful: most sequences repeat a number |
| Swap-in-place backtracking | O(n ยท n!) | O(n) stack | Original drill; non-lexicographic order |
| used[] backtracking | O(n ยท n!) | O(n) + output | Lexicographic for sorted input |
result.add(path)without copying stores the same list n! times โ every entry ends up empty after the final un-choose.- Forgetting
used[i] = falseafter the recursive call: only the first permutation is ever produced. path.remove(path.size() - 1)is index-based;path.remove(Integer.valueOf(x))also works, butpath.remove(x)with an int x removes by INDEX.- With duplicates (Permutations II): sort, then skip
iwhennums[i] == nums[i-1] && !used[i-1].
Combination Sum โ Unordered Choices with Reuse
candidates and a target, return all unique combinations whose numbers sum to target. The same number may be chosen any number of times; combinations differing only in order count once.Without the start index the tree contains every ordering and you would need a Set of sorted lists to remove duplicates โ exponentially more work. Passing i (not i + 1) to the recursive call is what allows reuse. The original drill did not sort, so it could only skip a too-large candidate and still looped over the rest; sorting a copy and using break cuts all larger candidates in one comparison. The search tree for the example has just ten nodes:
| Approach | Time | Space | Notes |
|---|---|---|---|
| All orderings + Set dedup | Exponential ร orderings | Large | Generates each multiset many times |
| Start index, no sort | O(nT/m + 1) | O(T/m) | T = target, m = smallest candidate |
| Start index + sort + break | O(nT/m + 1) worst | O(T/m) | Same bound, far fewer nodes in practice |
- The original stated O(2n) time; because candidates repeat, the depth is up to T/m, giving O(nT/m + 1) in the worst case.
- Recursing with
i + 1forbids reuse (that is Combination Sum II); recursing with0re-creates every ordering. - Sorting the caller's array in place is a side effect โ the fixed code sorts a clone.
- Combination Sum II (each number once, input has duplicates): recurse with i + 1 and skip
i > start && c[i] == c[i-1].
Generate Parentheses โ Build Only Valid Prefixes
n pairs of parentheses, generate all combinations of well-formed (balanced) parentheses.open โค n and close โค open. So add "(" while open < n, and add ")" only while close < open. Every leaf at length 2n is then automatically valid โ no final check.
Brute force builds all 22n strings of "(" and ")" and validates each in O(n). The constrained version never creates an invalid prefix, so the work is proportional to the number of valid strings, the Catalan number C(n) (5 for n = 3, 16796 for n = 10). The original drill concatenated curr + "(", allocating a new String at every node; a single StringBuilder with append / deleteCharAt is the idiomatic choose / un-choose.
| n | Catalan C(n) = results | All 22n strings |
|---|---|---|
| 1 | 1 | 4 |
| 2 | 2 | 16 |
| 3 | 5 | 64 |
| 4 | 14 | 256 |
| 8 | 1430 | 65536 |
| Approach | Time | Space | Notes |
|---|---|---|---|
| All strings + validate | O(22n ยท n) | O(n) | Mostly invalid strings |
| String concatenation backtracking | O(C(n) ยท n) plus copies | O(n2) live strings | Original drill |
| StringBuilder backtracking | O(C(n) ยท n) โ O(4n/โn) | O(n) + output | Shown in the code |
- Using
close < ninstead ofclose < opengenerates ")(" prefixes. - With a StringBuilder, forgetting
deleteCharAtafter the first branch leaks a "(" into the ")" branch. - Checking validity only at the leaves is correct but exponentially slower โ prune at every step.
- The order of the two branches decides the output order; "(" first gives the lexicographic order shown.
Min Stack โ Remember the Answer at Push Time
push, pop, top and getMin (retrieve the minimum element), each in O(1) time.(value, min so far). Popping restores the previous minimum for free, because it is already in the new top.
Scanning the stack in getMin is O(n). Keeping a single min variable fails on pop โ after removing the minimum you do not know the next one. The snapshot-per-entry design makes every operation O(1). The original drill used the classic two-stack variant (push onto the min stack when val <= minStack.peek()) on the legacy synchronized java.util.Stack; the version below uses ArrayDeque and one stack of pairs, which has no equality pitfalls.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Scan on getMin | O(n) getMin | O(n) | Other ops O(1) |
| Two stacks (value + min) | O(1) all | O(n) | Min stack only grows on new minimum or tie |
| Pairs (value, min) | O(1) all | O(n) | Simplest invariant; shown in the code |
- Two-stack variant with
Deque<Integer>:stack.pop() == minStack.peek()compares references โ false for values outside -128..127. Use.equalsor unbox tointfirst. - Two-stack variant: pushing onto the min stack only when
val < min(strict) breaks on duplicate minima โ pop removes the min too early. - Prefer
ArrayDequeoverjava.util.Stack, which extends the synchronizedVector. - LeetCode guarantees pop/top/getMin are never called on an empty stack; in production, throw
NoSuchElementExceptionexplicitly.
LRU Cache โ Compose a Map and a List
capacity. get(key) returns the value or -1; put(key, value) inserts or updates, and when the cache is full it first evicts the least recently used key. Both must run in O(1) average time.HashMap<key, Node> gives O(1) lookup. A doubly linked list kept in recency order gives O(1) "move to front" and "remove from back" โ but only because the map hands us the node directly, so we never search the list. Sentinel head and tail nodes remove every null check.
An array or singly linked list needs O(n) to find and move an entry; a TreeMap keyed by timestamp gives O(log n). The map + doubly-linked-list composition is O(1) for both operations. Each node stores its key so that when the tail node is evicted we can also delete it from the map. A get counts as a use and moves the node to the front.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Array / list scan | O(n) per op | O(capacity) | Find + shift on every access |
| TreeMap by timestamp | O(log n) per op | O(capacity) | Two maps to keep in sync |
| HashMap + doubly linked list | O(1) per op | O(capacity) | Shown in the code; LinkedHashMap does the same internally |
- Not storing the key in the node: on eviction you cannot remove the entry from the map.
- Forgetting that
getmust also move the node to the front โ then eviction removes a recently read key. - Updating an existing key without moving it to the front, or evicting on an update when the cache is full (size does not grow).
- Pointer order in
addFront: set the new node's prev/next first, thenhead.next.prev, thenhead.nextโ reversing the last two loses the old first node.
K Closest Points โ Bounded Heap
[x, y] on a plane and an integer k, return the k points closest to the origin (Euclidean distance). The answer may be in any order.Math.sqrt and floating-point error โ the order is the same.
Sorting all points by distance is O(n log n). A size-k max-heap is O(n log k) and O(k) memory, and also works on a stream where n is unknown. Quickselect is O(n) on average but O(n2) worst case and reorders the input. The original drill claimed the output order was closest-first, but polling a max-heap returns the farthest first โ the fixed version fills the result from the back.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort by distance | O(n log n) | O(n) or O(log n) | Simplest; fine for small n |
| Quickselect | O(n) average, O(n2) worst | O(1) extra | Mutates input; result unordered |
| Max-heap of size k | O(n log k) | O(k) | Streaming-friendly; shown in the code |
- Using a min-heap of all n points and polling k times is O(n + k log n) โ fine, but it stores everything; the bounded max-heap is the "top-k" pattern interviewers look for.
- The original used a subtraction comparator. It happens to be safe here (both squared distances are non-negative ints), but
a - boverflows for mixed-sign values โ makeInteger.comparethe habit. - Comparing
Math.sqrtdoubles works but is slower and invites precision bugs. Squared distances are exact ints for |x|, |y| โค 104; beyond about 32,000 per coordinate switchdist2tolong. - Ties at the k-th distance: any choice is accepted; do not add tie-breaking logic unless asked.
All Six Drills at a Glance
The six drills split cleanly: three enumerate a search tree and differ only in how choices are generated (all unused items, items from a start index, characters allowed by two counters); three maintain an invariant that makes the expensive query free. Space for the search drills excludes the output itself.
| # | Drill | Pattern | Time | Space | Level |
|---|---|---|---|---|---|
| 95 | Permutations | Backtracking + used[] | O(n ยท n!) | O(n) | Medium |
| 96 | Combination Sum | Backtracking + start index + sorted break | O(nT/m+1) | O(T/m) | Medium |
| 97 | Generate Parentheses | Constrained construction | O(4n/โn) | O(n) | Medium |
| 98 | Min Stack | Entry + min snapshot | O(1) per op | O(n) | Medium |
| 99 | LRU Cache | HashMap + doubly linked list | O(1) per op | O(capacity) | Medium |
| 100 | K Closest Points to Origin | Bounded max-heap | O(n log k) | O(k) | Medium |
- Copy at the leaf, undo after the call โ the two lines that make a shared path safe.
- Ordering kills duplicates โ a start index (or sorted skip rule) is cheaper than any dedup set.
- Prune before you recurse โ reject a prefix as soon as it cannot lead to a valid answer.
- Design = invariant + O(1) maintenance โ and compose structures when one is not enough.
- Previous page: 09 ยท Dynamic Programming โ when only the count or the optimum is needed.
- Start again: 01 ยท Arrays & Hashing โ the full set of 100 drills is complete.
- Theory: Backtracking guide, Stack, HashMap, Linked List and Heap.
- More problems: Backtracking problem set, Stack problem set and Heap problem set.
โ 09 ยท Dynamic Programming All drills 01 ยท Arrays & Hashing โบ