Recursion and Backtracking Master Recap — Templates, Pattern Cues, and Top 30 Problems

Sanjeev SharmaSanjeev Sharma
9 min read

Advertisement

Problem Statement

This is the recap article for the recursion and backtracking series. It collects every reusable template, every pattern cue, and every duplicate-handling rule into one place so you can review quickly before an interview.

Constraints:

  • All templates assume the choose-explore-unchoose discipline
  • All recursive calls must respect either a start index (subsets, combinations) or a used array (permutations)
  • Pruning must never drop valid solutions
  • Snapshots are mandatory before storing a state
Input:  Any "generate all", "find all valid", "count arrangements" problem
Output: A working backtracking implementation in roughly 15 lines

Why This Problem Matters

Backtracking is one of the highest-leverage topics in DSA interviews. Amazon, Google, Meta, and Microsoft together ask backtracking problems in roughly one of every three onsite loops. The templates do not change — only the validity check and the pruning rule do — which means a single recap session can cover a large slice of your interview surface area.

A well-prepared candidate solves any of LC 39, 40, 46, 47, 78, 90, 51, 79, 17, 22, 131 in under 12 minutes once the pattern is recognized. The bottleneck is almost never the code; it is recognizing which template applies and which pruning rule to layer on top.

This recap is structured for spaced repetition. Skim the templates the day before an interview, walk through one problem from the top-30 list, and you will be ready.

The Core Insight

Every backtracking problem has the same skeleton: a base case that records or returns the answer, an optional pruning step, a loop over candidate choices, and the choose-explore-unchoose triple. Different problems differ only in three places — what defines completeness, what counts as a candidate, and what pruning is correctness-preserving.

Visual Dry Run

CuePatternTemplate
"generate all subsets"Subsets / Power Setbt(start, current)
"generate all permutations"Permutationsbt(current) with used array
"find all combinations summing to..."Combination Sumbt(start, current, remaining)
"place N pieces"N-Queensbt(row, col_set, diag_sets)
"fill grid"Sudokubt(cell_index, constraints)
"find all paths"Word Search / Mazebt(r, c) with mark/unmark
"partition into valid groups"Palindrome Partitionbt(start, parts)

Solution (Optimal)

class Solution:
    # 1. Subsets — include or exclude each element
    def subsets(self, nums):
        result = []
        def bt(start, current):
            result.append(current[:])
            for i in range(start, len(nums)):
                current.append(nums[i])
                bt(i + 1, current)
                current.pop()
        bt(0, [])
        return result
 
    # 2. Permutations — used array, every element a candidate at every slot
    def permute(self, nums):
        result, used = [], [False] * len(nums)
        def bt(current):
            if len(current) == len(nums):
                result.append(current[:])
                return
            for i in range(len(nums)):
                if used[i]:
                    continue
                used[i] = True
                current.append(nums[i])
                bt(current)
                current.pop()
                used[i] = False
        bt([])
        return result
 
    # 3. Combination Sum — sort, prune by remaining, allow reuse via i not i+1
    def combinationSum(self, candidates, target):
        candidates.sort()
        result = []
        def bt(start, current, remaining):
            if remaining == 0:
                result.append(current[:])
                return
            for i in range(start, len(candidates)):
                if candidates[i] > remaining:
                    break  # sorted, so all later candidates are too large
                current.append(candidates[i])
                bt(i, current, remaining - candidates[i])  # i for reuse
                current.pop()
        bt(0, [], target)
        return result
 
    # 4. N-Queens — column and diagonal sets prune O(n!) to manageable
    def nQueens(self, n):
        result = []
        cols, d1, d2 = set(), set(), set()
        board = [['.'] * n for _ in range(n)]
        def bt(r):
            if r == n:
                result.append([''.join(row) for row in board])
                return
            for c in range(n):
                if c in cols or (r - c) in d1 or (r + c) in d2:
                    continue
                board[r][c] = 'Q'
                cols.add(c); d1.add(r - c); d2.add(r + c)
                bt(r + 1)
                board[r][c] = '.'
                cols.discard(c); d1.discard(r - c); d2.discard(r + c)
        bt(0)
        return result
// 1. Subsets
var subsets = function(nums) {
    const result = [];
    const bt = (start, current) => {
        result.push([...current]);
        for (let i = start; i < nums.length; i++) {
            current.push(nums[i]);
            bt(i + 1, current);
            current.pop();
        }
    };
    bt(0, []);
    return result;
};
 
// 2. Permutations
var permute = function(nums) {
    const result = [];
    const used = new Array(nums.length).fill(false);
    const bt = (current) => {
        if (current.length === nums.length) {
            result.push([...current]);
            return;
        }
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            current.push(nums[i]);
            bt(current);
            current.pop();
            used[i] = false;
        }
    };
    bt([]);
    return result;
};
 
// 3. Combination Sum
var combinationSum = function(candidates, target) {
    candidates.sort((a, b) => a - b);
    const result = [];
    const bt = (start, current, remaining) => {
        if (remaining === 0) {
            result.push([...current]);
            return;
        }
        for (let i = start; i < candidates.length; i++) {
            if (candidates[i] > remaining) break;
            current.push(candidates[i]);
            bt(i, current, remaining - candidates[i]);
            current.pop();
        }
    };
    bt(0, [], target);
    return result;
};

Time: Pattern-dependent — see complexity table below Space: O(depth) recursion plus O(output_size) for storing results

Duplicate Handling Rules

SituationRule
Subsets with duplicatesSort then if i > start and nums[i] == nums[i-1] continue
Permutations with duplicatesSort then if i > 0 and nums[i] == nums[i-1] and not used[i-1] continue
Combination Sum (no reuse)Pass i + 1 to the recursive call
Combination Sum (reuse allowed)Pass i to the recursive call
Bucket / partition equal subsetsTrack a per-level seen set of bucket values

Complexity Quick Reference

ProblemTimeNote
Subsets, n elementsO(2^n * n)Snapshot copy each
Permutations, n elementsO(n! * n)Snapshot copy each
Combinations C(n, k)O(C(n, k) * k)k is selection size
N-Queens n by nO(n!) before pruningDiagonal sets cut deeply
Sudoku 9 by 9O(9^81) worstConstraint prop makes it fast
Word Search length L on m by n gridO(m * n * 4^L)DFS with mark
Combination Sum target TO(T^(T / min))With sort + prune

Common Mistakes

  • Forgetting to copy the current state when recording — every later mutation poisons the result
  • Skipping the unchoose step — sibling branches inherit dirty state
  • Using start index when a used array is needed (or vice versa)
  • Applying duplicate-skip without sorting first — adjacency is required
  • Confusing reuse (i) versus single-use (i + 1) in combination problems
  • Off-by-one errors in the pruning bound — always verify on a small case

Interview Tips

  • Always state the four-part skeleton before writing code
  • Draw the decision tree for n=3 or n=4 to verify your understanding
  • Articulate the pruning rule and prove it preserves correctness
  • Mention the bitmask alternative when n is small (less than 22)
  • Estimate complexity in terms of branching factor and depth, not just big-O

Follow-up Questions

  • When does DP replace backtracking? Hint: counting or boolean queries with overlapping subproblems.
  • How do you parallelize backtracking? Hint: distribute root-level branches across workers.
  • How do you stream solutions instead of storing all of them? Hint: yield in Python or callback in JavaScript.
  • How does iterative deepening DFS apply? Hint: bounded depth then increase, useful for game-tree search.
  • When is BFS-on-states better than DFS? Hint: shortest-path enumeration like LC 301 Remove Invalid Parentheses.

Top 30 Backtracking LeetCode Problems

LCProblemPattern
17Letter Combinations of a Phone NumberRecursive tree
22Generate ParenthesesCounter constraints
37Sudoku SolverCell-by-cell constraints
39Combination SumReuse with start index
40Combination Sum IINo reuse plus dup-skip
46PermutationsUsed array
47Permutations IIUsed plus dup-skip
51N-QueensRow plus three sets
52N-Queens IIBitmask count
77Combinationsk of n with prune
78SubsetsInclude or exclude
79Word SearchGrid DFS plus unmark
90Subsets IISort plus dup-skip
93Restore IP AddressesSegment validity
131Palindrome PartitioningPrecomputed palindrome plus bt
140Word Break IIMemoized backtracking
212Word Search IITrie plus grid DFS
216Combination Sum IIIk from 1 to 9
282Expression Add OperatorsOperator insertion
301Remove Invalid ParenthesesBFS minimum plus DFS all
306Additive NumberPartition by additive rule
320Generalized AbbreviationChar-level include or exclude
351Android Unlock PatternsGrid DFS with skip rules
401Binary WatchPopcount combinations
416Partition Equal Subset SumDP not bt
425Word SquaresTrie-guided bt
473Matchsticks to Squarek-bucket partition
526Beautiful ArrangementPosition-number validity
698Partition K Equal SubsetsBucket dedup
784Letter Case PermutationToggle-case tree

Key Takeaways

  • Backtracking has only a handful of templates — recognition matters more than memorization
  • Subsets uses start index, permutations uses used array, combinations uses both
  • Always snapshot before storing; always undo before exiting the loop iteration
  • Sort first when applying any duplicate-skip rule
  • Reuse vs no-reuse in combinations is the difference between passing i and i + 1
  • Pruning must be correctness-preserving — never drop a valid path
  • The four-part skeleton (base, prune, loop, choose-explore-unchoose) covers every problem in this series

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading