181 Backtracking template: choose-explore-unchoose 15s

Every backtracking problem follows choose → explore → unchoose. The only things that change are the state, choices, and goal condition.

BACKTRACKING TEMPLATE
─────────────────────
def backtrack(state, choices):
    if is_goal(state):
        result.append(state[:])  # copy!
        return

    for choice in choices:
        if not is_valid(choice, state):
            continue

        # 1. CHOOSE
        state.append(choice)      # or modify state

        # 2. EXPLORE
        backtrack(state, next_choices)

        # 3. UNCHOOSE (undo mutation)
        state.pop()               # or restore state

CRITICAL POINTS:
  □ Copy state on goal (state[:] or list(state))
  □ Always undo mutations (symmetric choose/unchoose)
  □ Prune early with is_valid before recursing
  □ Track 'start index' for combinations to avoid dupes
182 Pruning strategies: cut branches early 15s

Pruning transforms exponential backtracking into something practical. Always ask: can I reject this branch without exploring it?

PRUNING STRATEGIES
──────────────────
1. CONSTRAINT PRUNING — skip invalid choices early
   if not is_valid(choice): continue
   Example: N-Queens skip attacked cells

2. SORTED + SKIP DUPLICATES
   nums.sort()
   if i > start and nums[i] == nums[i-1]: continue
   Example: Combination Sum II, Subsets II

3. REMAINING SUM CHECK — can we still reach target?
   if remaining < 0: return        # overshot
   if remaining < min_choice: return  # can't reach

4. BOUND CHECK — is best possible better than known?
   if optimistic_bound <= best_so_far: return
   Example: branch and bound

5. SYMMETRY BREAKING — avoid symmetric solutions
   Fix first element, only explore from there
   Example: N-Queens first queen in left half

ORDER: sort choices so pruning triggers earlier.
       Try most constrained variable first (MRV).
183 Backtracking vs DP: when to use which 15s

DP counts/optimizes. Backtracking enumerates. If you need all solutions, backtracking. If you need the best/count, DP.

BACKTRACKING vs DP — DECISION GUIDE
────────────────────────────────────
USE BACKTRACKING when:
  □ Need ALL solutions (not just count/optimal)
  □ Solution is a sequence of choices (permutation, path)
  □ Constraints are complex (placement rules, validity)
  □ Search space can be pruned effectively
  Examples: N-Queens, Sudoku, all permutations

USE DP when:
  □ Need COUNT or OPTIMAL VALUE (not all solutions)
  □ Overlapping subproblems exist
  □ Optimal substructure holds
  □ State space is polynomial
  Examples: coin change, LCS, knapsack

BOTH work but pick wisely:
  "Number of ways to..." → usually DP (count)
  "Find all ways to..."  → usually backtracking (enumerate)
  "Min cost to..."       → usually DP (optimize)

HYBRID: precompute with DP, enumerate with backtracking
  Example: palindrome partitioning (DP palindrome check
           + backtracking to list all partitions)
184 Permutation vs combination vs subset: decision tree 15s

Three core enumeration patterns. The only differences: does order matter, how many to pick, and the start index.

PERMUTATION vs COMBINATION vs SUBSET
─────────────────────────────────────
         ORDER     PICK ALL?   START IDX?
PERM     matters   yes (n)     no (use visited[])
COMBO    no        k items     yes (avoid revisit)
SUBSET   no        any count   yes (avoid revisit)

TEMPLATES:
  Permutations: for i in range(n):
                  if not used[i]: choose i
  Combinations: for i in range(start, n):
                  choose i, recurse with start=i+1
  Subsets:      for i in range(start, n):
                  choose i, record at every node

DUPLICATE HANDLING (sorted input):
  if i > start and nums[i] == nums[i-1]: continue

REUSE ALLOWED (combination sum):
  recurse with start=i instead of start=i+1

QUICK CHECK:
  "order matters?" → permutation → O(n!)
  "fixed size k?"  → combination → O(C(n,k))
  "all sizes?"     → subset → O(2^n)
185 N-Queens valid check 10s

Store queen positions as board[row] = col. Check column clash and diagonal clash (|col_diff| == row_diff).

def is_valid(board, row, col, n):
    for i in range(row):
        if board[i] == col:          return False  # same column
        if abs(board[i]-col) == row-i: return False  # diagonal
    return True

# board[i] = column of queen in row i
186 N-Queens solve template 15s

Place queens row by row, checking column and diagonal conflicts. Classic backtracking with implicit undo.

def solveNQueens(n):
    res = []
    board = [-1] * n
    def backtrack(row):
        if row == n:
            res.append(['.'*c + 'Q' + '.'*(n-c-1) for c in board])
            return
        for col in range(n):
            if all(board[i] != col and abs(board[i]-col) != row-i
                   for i in range(row)):
                board[row] = col
                backtrack(row + 1)
                board[row] = -1
    backtrack(0)
    return res
187 Sudoku solver 15s

Find empty cell, try digits 1-9 with constraint checks (row, col, 3x3 box). Backtrack on failure.

def solveSudoku(board):
    def is_valid(r, c, ch):
        br, bc = 3*(r//3), 3*(c//3)
        for i in range(9):
            if board[r][i] == ch: return False
            if board[i][c] == ch: return False
            if board[br + i//3][bc + i%3] == ch: return False
        return True

    def solve():
        for i in range(9):
            for j in range(9):
                if board[i][j] == '.':
                    for ch in '123456789':
                        if is_valid(i, j, ch):
                            board[i][j] = ch
                            if solve(): return True
                            board[i][j] = '.'
                    return False
        return True
    solve()
188 Word search on grid 15s

Grid backtracking: mark cell as visited, explore 4 directions, unmark on backtrack. O(m*n*4^L).

def exist(board, word):
    m, n = len(board), len(board[0])
    def dfs(i, j, k):
        if k == len(word): return True
        if i < 0 or i >= m or j < 0 or j >= n: return False
        if board[i][j] != word[k]: return False
        tmp, board[i][j] = board[i][j], '#'  # mark visited
        for di, dj in (0,1),(0,-1),(1,0),(-1,0):
            if dfs(i+di, j+dj, k+1): return True
        board[i][j] = tmp  # unmark
        return False
    for i in range(m):
        for j in range(n):
            if dfs(i, j, 0): return True
    return False
189 Combination sum (reuse allowed) 15s

Reuse allowed: recurse with start=i (not i+1). Sort + break when candidate > remaining for pruning.

def combinationSum(candidates, target):
    res = []
    def backtrack(start, path, remaining):
        if remaining == 0:
            res.append(path[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] > remaining: break
            path.append(candidates[i])
            backtrack(i, path, remaining - candidates[i])  # i, not i+1
            path.pop()
    candidates.sort()
    backtrack(0, [], target)
    return res
190 Combination sum II (no reuse, skip dupes) 15s

No reuse: recurse with i+1. Skip duplicates: if i > start and same as previous, skip. Must sort first.

def combinationSum2(candidates, target):
    res = []
    candidates.sort()
    def backtrack(start, path, remaining):
        if remaining == 0:
            res.append(path[:])
            return
        for i in range(start, len(candidates)):
            if candidates[i] > remaining: break
            if i > start and candidates[i] == candidates[i-1]: continue
            path.append(candidates[i])
            backtrack(i + 1, path, remaining - candidates[i])
            path.pop()
    backtrack(0, [], target)
    return res
191 Palindrome partitioning backtrack 15s

Try every prefix as a partition. If it's a palindrome, recurse on the rest. Enumerate all valid partitions.

def partition(s):
    res = []
    def backtrack(start, path):
        if start == len(s):
            res.append(path[:])
            return
        for end in range(start + 1, len(s) + 1):
            sub = s[start:end]
            if sub == sub[::-1]:
                path.append(sub)
                backtrack(end, path)
                path.pop()
    backtrack(0, [])
    return res
192 Restore IP addresses 15s

Place 3 dots in the string to create 4 parts. Each part: 1-3 digits, no leading zeros, value 0-255.

def restoreIpAddresses(s):
    res = []
    def backtrack(start, parts):
        if len(parts) == 4:
            if start == len(s):
                res.append('.'.join(parts))
            return
        for length in range(1, 4):
            if start + length > len(s): break
            seg = s[start:start+length]
            if (seg[0] == '0' and len(seg) > 1) or int(seg) > 255:
                continue
            backtrack(start + length, parts + [seg])
    backtrack(0, [])
    return res
193 Letter combinations of phone number 15s

Each digit maps to letters. At each position, branch on all mapped letters. O(4^n) worst case.

def letterCombinations(digits):
    if not digits: return []
    phone = {'2':'abc','3':'def','4':'ghi','5':'jkl',
             '6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
    res = []
    def backtrack(i, path):
        if i == len(digits):
            res.append(''.join(path))
            return
        for ch in phone[digits[i]]:
            path.append(ch)
            backtrack(i + 1, path)
            path.pop()
    backtrack(0, [])
    return res
194 Partition to K equal sum subsets 15s

Try placing each number into k buckets. Prune: skip if bucket overflows, skip symmetric bucket states.

def canPartitionKSubsets(nums, k):
    total = sum(nums)
    if total % k: return False
    target = total // k
    nums.sort(reverse=True)
    buckets = [0] * k

    def backtrack(i):
        if i == len(nums): return all(b == target for b in buckets)
        seen = set()
        for j in range(k):
            if buckets[j] + nums[i] > target: continue
            if buckets[j] in seen: continue  # prune symmetric
            seen.add(buckets[j])
            buckets[j] += nums[i]
            if backtrack(i + 1): return True
            buckets[j] -= nums[i]
        return False
    return backtrack(0)
195 Path with maximum gold (grid backtrack) 15s

Grid backtracking: visit cells with gold, mark visited by zeroing, restore on backtrack. Try every start.

def getMaximumGold(grid):
    m, n = len(grid), len(grid[0])
    def dfs(i, j):
        if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == 0:
            return 0
        val = grid[i][j]
        grid[i][j] = 0  # mark visited
        best = 0
        for di, dj in (0,1),(0,-1),(1,0),(-1,0):
            best = max(best, dfs(i+di, j+dj))
        grid[i][j] = val  # unmark
        return val + best

    return max(dfs(i, j)
               for i in range(m) for j in range(n)
               if grid[i][j] > 0)
267 Permutations backtrack 20s

Permutations use a used[] mask instead of start-index. Each level tries all unused elements. O(n!) results.

def permute(nums):
    res = []
    def bt(path, used):
        if len(path) == len(nums):
            res.append(path[:])
            return
        for i in range(len(nums)):
            if not used[i]:
                used[i] = True
                path.append(nums[i])
                bt(path, used)
                path.pop()
                used[i] = False
    bt([], [False]*len(nums))
    return res
268 Subsets (include/exclude) 15s

At each index, branch into include or exclude. Generates all 2^n subsets. Alternative: iterative bit-mask approach.

def subsets(nums):
    res = []
    def bt(i, path):
        if i == len(nums):
            res.append(path[:])
            return
        bt(i+1, path)             # exclude
        path.append(nums[i])
        bt(i+1, path)             # include
        path.pop()
    bt(0, [])
    return res