CMD Guide
HomeDSA

Backtracking

Step 18 in the DSA path · 2 concepts · 4 problems

0 / 6 complete

📘 Learn Backtracking from zero

Backtracking explores a decision tree by building a candidate one choice at a time, and the instant a partial candidate cannot lead to a valid full solution, it abandons it and undoes the last choice (the "backtrack") to try the next option. The skeleton is always: choose → recurse → un-choose. Reach for it when a problem asks you to enumerate or count all valid configurations — permutations, combinations, subsets, partitions, board placements, or string decompositions — especially when each position has a small set of choices and constraints prune branches early. The trigger phrase to listen for: "find all ways / all combinations / all valid arrangements."

✨ Added by the guide to build intuition — not from the source course.

🏗️ Visual walkthrough — trace it step by step

Step 0 — The problem and the engine

Canonical example: Letter Combinations of a Phone Number for digits = "23". Digit 2 maps to abc, digit 3 maps to def. We must output every string formed by picking one letter per digit, so backtracking is the natural fit (one decision per position, small branching factor).

digits = " 2    3 "
          |    |
          v    v
        a b c  d e f      <- choices per slot

map = { '2': "abc", '3': "def" }

State we carry:
  index  -> which digit we are deciding (0..len)
  path   -> letters chosen so far (a stack)
  result -> completed combinations

INITIAL:
  index  = 0
  path   = [ ]            (empty)
  result = [ ]            (empty)

The path is a mutable stack we push to before recursing and pop from after — that pop is the backtrack. index == len(digits) is the base case: a full combination is ready to record.

Step 1 — Choose 'a' at index 0

At index = 0 the digit is '2' → choices a,b,c. We pick the first, 'a': push it onto path and recurse to index = 1. This is safe because every letter of '2' is a legal first character, so no pruning is needed yet — we just commit and dive deeper.

index = 1   (just descended)

path stack (bottom -> top):
  +---+
  | a |   <- chose 'a' for digit '2'
  +---+

path as string: "a"

remaining digit to decide: '3' -> d e f
result = [ ]

We are now one level deep. The recursion will return here later so we can try 'b' and 'c' — that is the breadth at this level, explored one branch at a time.

Step 2 — Choose 'd' at index 1 → base case → record "ad"

At index = 1 the digit is '3' → choices d,e,f. Pick 'd', push it, recurse to index = 2. Now index == len("23") == 2: base case hit. The path is a complete combination, so we copy it into result. Copying (not aliasing) is safe and required, because path will keep mutating after we return.

index = 2  ==  len(digits)  -> BASE CASE

path stack:
  +---+
  | d |   <- top  (digit '3')
  +---+
  | a |   <- bottom (digit '2')
  +---+
path as string: "ad"   --copy-->  result

result = [ "ad" ]

No further choices exist past the last digit, so we stop descending and unwind back to index = 1 to try the siblings of 'd'.

Step 3 — Backtrack, then choose 'e' and 'f' → "ae", "af"

Returning from the base case, we pop 'd' (un-choose) so path is just "a" again. The loop at index = 1 advances to 'e': push, recurse, hit base case, record "ae", pop. Then 'f': record "af", pop. The pop after each child is what keeps the stack clean for the next sibling — without it, letters would leak across branches.

back at index = 1, cycling choices d -> e -> f

after 'e':  path = [a][e] -> record "ae", then pop e
after 'f':  path = [a][f] -> record "af", then pop f

path stack now (all '3'-choices exhausted):
  +---+
  | a |   <- only the '2'-choice remains
  +---+

result = [ "ad", "ae", "af" ]

Digit '3' is fully explored under the prefix "a". We return to index = 0 to advance past 'a'.

Step 4 — Backtrack to index 0, choose 'b'

The index = 1 call finishes, so we pop 'a' at index = 0, emptying path. The loop at index = 0 advances from 'a' to 'b': push 'b' and recurse into index = 1 again. The subtree under 'b' is explored with exactly the same machinery, producing "bd", "be", "bf".

back at index = 0, advanced 'a' -> 'b'

path stack:
  +---+
  | b |   <- fresh '2'-choice, 'a' was popped
  +---+
path as string: "b"

(recurse index=1 over d,e,f as before)

result = [ "ad", "ae", "af",
           "bd", "be", "bf" ]

Notice the stack never grows beyond len(digits) = 2 at any moment — backtracking gives us all 9 strings using O(depth) extra space, not O(number of results).

Step 5 — Choose 'c', exhaust the tree

Pop 'b', advance to the last choice 'c' at index = 0, and recurse one final time over d,e,f. This adds "cd", "ce", "cf". After the 'c' subtree returns and 'c' is popped, the loop at index = 0 has no more letters — the recursion fully unwinds and path is empty again, matching its initial state.

index=0 choice 'c' -> recurse d,e,f

final path stack: (empty, all popped)
  +---+
  |   |
  +---+

result = [ "ad","ae","af",
           "bd","be","bf",
           "cd","ce","cf" ]   (3 x 3 = 9)

The empty final stack is the proof the algorithm is balanced: every push was matched by a pop, so no state leaked. That symmetry is the heart of correctness in backtracking.

Step 6 — Generalize: pruning makes it powerful

Phone letters never prune (all choices are always legal), but the same skeleton scales to constrained problems by adding a validity check before recursing. The earlier you reject, the more of the tree you skip.

backtrack(index, path):
  if index == END:                # base case
      result.add(copy(path))
      return
  for choice in choices(index):
      if NOT valid(choice, path):  # <-- PRUNE here
          continue                 #     skip dead branch
      path.push(choice)            # choose
      backtrack(index+1, path)     # explore
      path.pop()                   # un-choose (backtrack)

Examples of valid():
  Permutations II : skip if choice used, OR
                    (choice == prev && prev not used)  // dedup
  Balanced parens : open

For Permutations II you sort first and skip a duplicate value whose identical predecessor is unused, killing duplicate subtrees. For Balanced Parentheses and Palindrome Partitioning the valid() guard rejects illegal prefixes immediately — same three-line choose/recurse/un-choose engine, different pruning rule. Master the symmetry and the guard, and all five lessons collapse into one pattern.

🎯 Guided practice

These two warm-ups isolate the two reusable levers — the start-index (for combinations/partitions) and the used-array dedup (for permutations with repeats) — that the in-scope problems (Permutations II, Letter Combinations, Generate Parentheses, Palindrome Partitioning) all reuse.

Problem 1 (easy) — Letter Combinations of a Phone Number, scaled down. Given digits = "23" and the phone mapping (2→abc, 3→def), return every string formed by picking one letter per digit.

  1. Frame the decision: the candidate is fixed-length (one char per digit), so you branch on position, not on a start index. At depth d, the branching factor is the number of letters on digits[d] (3 or 4).
  2. Define the recursion: backtrack(idx, path). Base case: when idx == len(digits), the candidate is complete — record "".join(path) (a fresh string, so no aliasing) and return.
  3. Choose / explore / un-choose: for each letter mapped to digits[idx]: append letter, recurse with backtrack(idx + 1, path), then pop it.
  4. Trace it: "" → a → ad ("ad") → pop → ae ("ae") → af ("af"), then back up and restart with b, then c. Output: ad, ae, af, bd, be, bf, cd, ce, cf — exactly 3×3 = 9 strings.
  5. The lesson: when every level consumes a fixed slot, the loop variable is the choice within a level and recursion advances by idx + 1. Edge case to state aloud: empty digits returns [], not [""].

Problem 2 (medium) — Generate Parentheses. Given n = 2, return all well-formed strings using n pairs. (LeetCode rates this Medium — note that, despite this set's "hard" label, the pattern is mid-tier; the rigor is in the pruning.)

  1. Spot the pattern: "return all well-formed strings" = enumerate-all + early pruning → backtracking. Counting them instead would be the Catalan number via DP; here we must emit the strings.
  2. State: backtrack(path, open, close) where open / close are counts placed so far.
  3. The pruning (the whole point): only add '(' while open < n; only add ')' while close < open. That second guard kills any prefix that would close more than it has opened — you never even build an invalid candidate, so there is no separate validity check at the leaf.
  4. Base case: when path has length 2n (equivalently open == close == n), record it and return.
  5. Trace n = 2: ( → (( → (() → (()) (record); backtrack to (() → ()( → ()() (record). Output: (()), ()() — the 2 Catalan-many strings.
  6. The lesson: the two count-guards encode the validity invariant directly into construct_candidates, so pruning happens at every node instead of via a reject-at-the-end test. This "make the invalid branch unconstructable" move is the same idea you reuse in Palindrome Partitioning, where you only recurse on a prefix after confirming it is a palindrome.

✨ Added by the guide — work these before the full problem set.

Lessons in this topic

🧠 Review & recall

Active recall is what moves a topic into long-term memory. Flip each card before revealing, then test yourself — your results are saved on this device.

Flashcard
What is the three-step skeleton of every backtracking algorithm, and what specific action is 'the backtrack'?
tap to reveal →
The skeleton is always choose -> recurse -> un-choose. You build a candidate one choice at a time and the instant a partial candidate can't lead to a valid solution you abandon it. The 'un-choose' (popping the last choice off the path) IS the backtrack.
💡 Choose, dive, undo — every push is matched by a pop.
Flashcard
What trigger phrases in a problem signal that backtracking is the right pattern?
tap to reveal →
Reach for backtracking when asked to enumerate or count ALL valid configurations — permutations, combinations, subsets, partitions, board placements, or string decompositions — especially with a small set of choices per position. Listen for 'find all ways / all combinations / all valid arrangements'.
💡 'All valid arrangements' = backtrack.
Flashcard
How does backtracking differ from Dynamic Programming, per the intro lesson?
tap to reveal →
DP also tries all solutions but uses memoization to pick the single most-optimized solution for optimization problems. Backtracking is used when MULTIPLE valid solutions are possible and you want to enumerate them, abandoning branches that violate constraints.
💡 DP = best one (optimize); backtracking = all of them (enumerate).
Flashcard
In Permutations II, what is the exact skip condition that avoids duplicate permutations, and what setup makes it work?
tap to reveal →
First sort nums. Then skip when used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1]). Sorting groups equal values, and skipping a duplicate whose identical predecessor is unused kills the duplicate subtree.
💡 Sort first; skip a dup whose twin-before-it is still unused.
Flashcard
What two count-based rules generate all N pairs of balanced parentheses, and why is no leaf-validity check needed?
tap to reveal →
Add '(' only while openCount < N; add ')' only while openCount > closeCount (open exceeds close). These guards make invalid prefixes unconstructable, so every completed string (openCount == closeCount == N) is already valid — no separate check at the leaf.
💡 Open if room left; close only if you owe a close.
Flashcard
What is the time and space complexity of the Permutations II backtracking solution?
tap to reveal →
Time is O(n x n!): there are n! permutations and each takes O(n) to build and store. Space is O(n x n!): storing all n! permutations of length n, plus a recursion stack, tempList, and used array each O(n).
💡 n! permutations, each n long — multiply for both time and space.
Q1. For the tree-planting example (plant Apple, Orange, Mango in a line; Apple and Orange may NOT be adjacent), which are the only two valid arrangements?
Q2. In Letter Combinations of a Phone Number, what is the base case that records a completed combination?
Q3. For digits = "23" (2->abc, 3->def), how many letter combinations are produced, and why?
Q4. In Palindromic Partitioning, what does the function compute and what is the answer for input "pp"?
Q5. Why does the brute-force recursive Palindromic Partitioning solution get improved with a 2D memoization table?