Home/SWE Hub/Backtracking
Back
๐Ÿ”™

Algorithms

Backtracking

Backtracking is a systematic way to search all possible solutions by building candidates incrementally and abandoning ('pruning') a candidate as soon as it is determined that the candidate cannot possibly lead to a valid solution. It is the engine behind solving Sudoku, N-Queens, word search on a grid, and generating all subsets/permutations. The universal template โ€” choose, explore, unchoose โ€” appears in 15-20% of hard LeetCode problems.

Recursion and call stackDFS on graphs/treesBig-O for exponential algorithmsBacktrackingDFSSubsetsPermutationsCombinationsN-QueensPruningRecursion

Core Concepts

  • 1The core template โ€” choose/explore/unchoose: every backtracking solution follows the same three-step loop. (1) Choose: make a decision (add element x to current path, place a queen in column c, etc.). (2) Explore: recurse deeper with that decision made. (3) Unchoose: undo the decision ('backtrack') before trying the next option. This undo step is what separates backtracking from plain DFS. The call stack implicitly manages all the branching โ€” each recursive call handles one decision level, and returning from it automatically leads to the next branch.
  • 2Subsets (power set): to generate all 2^n subsets of an array, use a start index to avoid revisiting earlier elements. At each call, the current path is a valid subset โ€” add it to results immediately. Then try adding each element from start..n-1 to extend the path. Crucially, no two calls at the same recursion depth consider the same element โ€” this is enforced by incrementing start. For subsets with duplicates (LC 90), sort first and skip arr[i] == arr[i-1] at the same recursion depth to avoid identical subsets.
  • 3Permutations: unlike subsets, permutations use every element exactly once and care about order. The classic approach: maintain a 'used' boolean array. At each depth, try every index not yet used. Choose it (mark used, add to path), recurse, then unchoose (mark unused, remove from path). Total time O(n ร— n!) โ€” n! permutations each of length n. For permutations with duplicates (LC 47), sort the array and add the pruning condition: skip arr[i] if i > 0 and arr[i] == arr[i-1] and not used[i-1]. This ensures that among duplicate values, the leftmost is always placed first.
  • 4Combinations: choose k elements from n without regard to order. The key pruning: at each step, if fewer than (k - path.length) elements remain in [start..n-1], it is impossible to reach size k, so prune. This reduces the constant factor substantially. The upper bound of the for loop is n - (k - path.length) + 1 (1-indexed) or equivalently n - (k - len(path)) (0-indexed with exclusive end). Combination sum (LC 39) allows reuse of the same element โ€” pass the same index i (not i+1) when recurring. Combination sum II (LC 40) forbids reuse and has duplicates โ€” sort and skip duplicates at the same depth.
  • 5N-Queens: place n queens on an nร—n board such that no two queens share a row, column, or diagonal. The key insight: since we place exactly one queen per row (recurse row by row), we only need to check column conflicts and diagonal conflicts. Track three sets: cols (occupied columns), diag1 (occupied r-c diagonals โ€” same value for '/' diagonals), diag2 (occupied r+c diagonals โ€” same value for '\' diagonals). A position (r, c) is safe if c not in cols, r-c not in diag1, r+c not in diag2. This reduces per-placement check from O(n) to O(1). Total solutions: 1, 0, 0, 2, 10, 4, 40, 92 for n=1..8.
  • 6Sudoku solver: the constraint propagation + backtracking combination. For each empty cell, try digits 1-9. Check row, column, and 3ร—3 box constraints. If valid, place the digit and recurse to the next empty cell. If the recursive call returns False (no solution found), remove the digit and try the next. The pruning is implicit: invalid placements are never explored. Optimization: always pick the empty cell with the fewest valid options (minimum remaining value heuristic) โ€” this can reduce the search space by orders of magnitude.
  • 7Word search on a grid: given a 2D grid and a word, determine if the word exists as a connected path (4-directional, no reuse). Start DFS from every cell that matches word[0]. Mark the current cell as visited (e.g., temporarily set to '#') before recursing to prevent reuse within the current path, then restore it after. Pruning: if word[0] appears far fewer times than word[-1], reverse the word โ€” searching from the rarer end prunes the search tree much earlier.
  • 8Palindrome partitioning (LC 131): partition a string s into substrings such that every substring is a palindrome. Enumerate all possible first substrings s[0..i] for i from 0 to n-1. If s[0..i] is a palindrome, add it to the path and recurse on s[i+1..]. Precompute a 2D palindrome table dp[i][j] in O(nยฒ) to check is_palindrome in O(1) during backtracking. Letter combinations of phone number (LC 17): for each digit, map it to its letters and recurse through the remaining digits โ€” this is the simplest tree of choices.
  • 9Time complexity of backtracking: always exponential in the worst case โ€” O(2^n) for subsets, O(n!) for permutations. In practice, pruning eliminates enormous portions of the search tree. The actual runtime depends heavily on the constraints. When discussing complexity in an interview, state the worst-case bound and then explain what pruning does: 'In the worst case O(n!) but pruning eliminates invalid branches early, making it feasible for n โ‰ค 20 or so.' For N-Queens, the actual number of solutions grows much slower than n! โ€” the constraint propagation keeps the tree manageable.

Key Patterns & Templates

Generic template: def bt(path, start): results.append(path[:]); for i in range(start, n): path.append(a[i]); bt(path, i+1); path.pop()
Subsets with duplicates: sort first; if i > start and a[i]==a[i-1]: continue
Permutations with used array: for i in range(n): if used[i]: continue; if i>0 and a[i]==a[i-1] and not used[i-1]: continue
Combinations pruning: for i in range(start, n-(k-len(path))+1): ... (prune when not enough elements remain)
N-Queens O(1) safety check: track cols, diag1 (r-c), diag2 (r+c) sets; safe if all three exclude (r,c)
Grid word search: mark cell '#' before recurse, restore after; prune on out-of-bounds or char mismatch
Palindrome partition: precompute dp[i][j] table; only recurse when s[start..i] is palindrome
Combination sum (reuse allowed): pass i not i+1 when recursing; prune if remaining > target

Worked Examples

Place n queens on an nร—n chessboard such that no two queens attack each other (no shared row, column, or diagonal). Return all distinct board configurations.

1Step 1 โ€” Model the search space: we place exactly one queen per row (since two queens in the same row always attack). So the decision at each recursive call is: which column to place the queen in row r. This gives a tree of branching factor n and depth n โ€” naively O(n^n), but constraints prune it heavily.
2Step 2 โ€” Constraint check: a queen at (r, c) attacks everything in its row (handled implicitly โ€” we never revisit a row), its column (track 'cols' set), its '/' diagonal (all cells on the same '/' diagonal share the value r-c โ€” track 'diag1' set), and its '\' diagonal (all cells share r+c โ€” track 'diag2' set). So a candidate (r, c) is safe iff c not in cols and r-c not in diag1 and r+c not in diag2.
3Step 3 โ€” Choose/explore/unchoose: add c to cols, r-c to diag1, r+c to diag2, set board[r][c]='Q'. Recurse to row r+1. Then undo all of that.
4Step 4 โ€” Base case: when row == n, all n queens are placed without conflict. Convert board to list of strings and add to results.
5Step 5 โ€” Time complexity: O(n!) in the worst case (n choices for row 0, at most n-2 for row 1 due to column+diagonal constraints, etc.). Space O(n) for the recursion stack plus O(nยฒ) for the board.
6Step 6 โ€” Bitmask optimization (bonus): represent cols, diag1, diag2 as integers. Use bitwise AND to find available columns: available = ((1<<n)-1) & ~(cols | diag1 | diag2). Isolate lowest set bit with bit & (-bit). This is 2-3ร— faster due to word-level parallelism.
โœ“ For n=4, there are 2 solutions. For n=8, there are 92. The O(1) set-based safety check (instead of scanning the board) is the key implementation detail. The choose/explore/unchoose skeleton is identical to every other backtracking problem โ€” only the pruning logic changes.

Common Mistakes

โš 

Forgetting to copy the path before adding to results: results.append(path) adds a reference to the mutable list โ€” when path is later modified by backtracking, the 'result' you saved changes too. Always use results.append(path[:]) or results.append(list(path)) to capture a snapshot.

โš 

Using i > 0 instead of i > start for deduplication: the condition to skip duplicates in subsets/combinations should be i > start, not i > 0. Using i > 0 incorrectly prevents using a duplicate value at a deeper recursion level even when it legitimately appears in a different position of the combination.

โš 

Forgetting to mark cells as visited in grid search (or forgetting to unmark them): in word search, if you don't mark the current cell as visited, the DFS might revisit it within the same path (e.g., spelling 'AA' by going right then left on a single 'A'). And if you don't unmark on backtrack, later searches from other starting cells will see false conflicts.

โš 

Not sorting before deduplication in permutations II: the dedup condition skips nums[i] when nums[i] == nums[i-1] and not used[i-1]. This only works if duplicates are adjacent โ€” which requires sorting first. Without sorting, duplicates might not be adjacent and the condition fails silently.

โš 

Calling bt(i+1, ...) instead of bt(i, ...) in combination sum (reuse allowed): since LC 39 allows the same element multiple times, you recurse with the same index i. Calling bt(i+1, ...) would prevent reuse, giving you combination sum II behavior instead. Conversely, calling bt(i, ...) in combination sum II would allow infinite reuse and infinite recursion.

Only works in the Electron app

<webview> is an Electron-only tag. Run npm run electron:dev to use this.

25:00Focus
0

25 min focus ยท 5 min break ยท long break every 4 sessions

The Ultimate Grind