3.7.17 · D4Algorithm Paradigms

Exercises — Backtracking problems — N-Queens, Sudoku solver, all permutations - subsets

2,228 words10 min readBack to topic

Prerequisites we lean on: DFS, the call stack, and later counting recursive work.


Level 1 — Recognition

L1.1 — Count the subsets

Recall Solution

Each element is a yes/no coin flip: in the subset or out. Four independent flips ⇒ . The formula is where is the count of elements — the same the parent note derived.

L1.2 — Count the permutations

Recall Solution

Fill 5 slots left to right. First slot: 5 choices. Second: 4 remaining. Then 3, 2, 1. .

L1.3 — Which set catches this attack?

Recall Solution

Compute each key. For : . For : . Equal ⇒ both land in the same diag (↘) bucket. (Check the others fail: columns ; anti-diagonals vs differ.) So the ==diag set== catches it. See the diagonal picture below.

Figure — Backtracking problems — N-Queens, Sudoku solver, all permutations - subsets

Level 2 — Application

L2.1 — Trace the subset tree

Recall Solution

The code recurses skip before take, so the first leaf reached is "skip everything".

  • bt(0) → skip 1 → bt(1) → skip 2 → bt(2): record []
  • back at bt(1): take 2 → bt(2): record [2]
  • back at bt(0): take 1 → bt(1) → skip 2 → record [1]
  • take 2 → record [1,2]

Order: . Exactly the parent's answer.

L2.2 — First two permutations emitted

Recall Solution

The loop always grabs the lowest available index first, so it dives 1 → 2 → 3.

  • First leaf: pick 1, pick 2, pick 3 → record .
  • Pop back to after "1,2"; no index left ⇒ pop 2. Now pick 3, then only 2 remains → record .

First two: .

L2.3 — Sudoku box index

Recall Solution

(box-row, since rows 6–8 form the bottom band). (box-col, columns 0–2 are the left band). Flatten the grid of boxes: — the bottom-left box.


Level 3 — Analysis

L3.1 — Why the copy path[:]?

Recall Solution

path is one single list object mutated in place (append/pop). res.append(path) stores four references to that same object. When the recursion fully unwinds, every push has a matching pop, so path ends empty. Result: res = [ [], [], [], [] ] — four aliases of the now-empty list. . The fix path[:] snapshots the current contents into a fresh list.

L3.2 — Counting valid checks in N-Queens (N=4)

Recall Solution

Queen at occupies: cols = {0}, diag = {0-0=0}, anti = {0+0=0}. Test each column c in row 1 (r=1):

  • : 0 in colspruned.
  • : r-c = 0, 0 in diagpruned (↘ diagonal).
  • : cols? no. r-c = -1 no. r+c = 3 no → survives.
  • : cols? no. r-c = -2 no. r+c = 4 no → survives.

Pruned: . Survivors: (2 columns). This is the pruning power — 2 of 4 branches die instantly.

L3.3 — Time cost of generating all permutations

Recall Solution

There are leaves. At each leaf we copy a list of length into res, costing . So total . Two factors: = number of complete orderings (leaves of the tree); = cost per snapshot. The internal branching work is dominated by the leaf copies. (See Time-Complexity-of-Recursive-Algorithms for how leaf-count times per-leaf-cost gives the total.)


Level 4 — Synthesis

L4.1 — Subsets with duplicates

Recall Solution

Sort first so equal elements are adjacent: [1,2,2]. Use the index-loop skeleton (choose a start, branch on each next element), and add the rule: within the same loop level, skip an element equal to the previous one.

def subsetsWithDup(nums):
    nums.sort(); res, path = [], []
    def bt(start):
        res.append(path[:])
        for i in range(start, len(nums)):
            if i > start and nums[i] == nums[i-1]:
                continue                      # skip duplicate sibling
            path.append(nums[i])
            bt(i+1)
            path.pop()
    bt(0); return res

Why i > start? The first copy of a value at each level is allowed (it starts a new branch); later equal siblings would re-generate an identical subtree, so we skip them. Distinct subsets: 6 of them (not 8).

L4.2 — Combination sum

Recall Solution

Backtrack over a running remain = target - sum. To avoid re-ordering the same multiset, pass a start index and never look left of it.

def combSum(cands, target):
    res, path = [], []
    def bt(start, remain):
        if remain == 0: res.append(path[:]); return
        if remain < 0: return                 # prune overshoot
        for i in range(start, len(cands)):
            path.append(cands[i])
            bt(i, remain - cands[i])          # i (not i+1): reuse allowed
            path.pop()
    bt(0, target); return res

Why bt(i, ...) not bt(i+1, ...)? Passing i again permits reusing the same candidate; passing start=i still blocks smaller-index numbers, killing reordered duplicates. Combinations summing to 8: . Check: ; . (No third: ? yes! — wait, recount below.)

Careful recount of all multisets of summing to 8:

  • all 2s: ✓ → [2,2,2,2]
  • ✓ → [2,3,3]
  • ✓ → [3,5]

So the true answer is 3 combinations. (The remain < 0 prune stops paths like that overshoot.)


Level 5 — Mastery

L5.1 — Count N-Queens solutions

Recall Solution

: only two valid full placements exist (they are mirror images). Columns per row: and . Count . : the classic count is distinct solutions. (For reference the sequence of solution counts for is — note are impossible, a degenerate case the solver handles by simply recording nothing.)

L5.2 — Prune Sudoku smarter (MRV heuristic)

Recall Solution

Use Minimum Remaining Values (MRV): among all empty cells, always fill the one with the fewest legal candidate digits first. Why it helps: a cell with only 1 or 2 options has a low branching factor, so recursing there splits the tree into few subtrees. Filling a low-freedom cell early fails fast on dead branches instead of after deep detours. This is a form of Branch-and-Bound / CSP variable ordering — it never removes a valid solution (we still try every candidate of the chosen cell), it only reorders which cell we commit to, so correctness is preserved while the average tree shrinks dramatically. A cell with 0 candidates is an immediate dead end — MRV detects it soonest, the ultimate early prune.

L5.3 — When is backtracking the wrong tool?

Recall Solution

Overlapping subproblems — e.g. counting paths in a grid, or Fibonacci-like recurrences. Backtracking re-explores the same partial state through different choice orders, redoing identical work exponentially. The winner is Dynamic-Programming: it memoizes each distinct subproblem once, turning exponential re-exploration into polynomial table lookups. Backtracking shines when subproblems are distinct (each partial board is unique); DP shines when they repeat.


Recall One-line self-test

Why must every choose have a matching un-choose? ::: Because the state (path, board, used) is shared and mutated; without restoring it, sibling branches inherit stale choices and emit garbage.