Where pruning stops being optional
Subsets, combinations, and permutations all have a manageable branching factor and a search space you can often afford to explore close to fully. Constraint satisfaction problems — N-Queens, Sudoku, Word Search — are different: the naive search space is so large that without aggressive pruning, your program does not finish in the age of the universe, let alone within a time limit. This subtopic is still the same choose → explore → un-choose template, but here the is_valid check inside the loop is no longer a minor guard — it is the entire algorithm. Everything you optimize is either "how cheaply can I check validity" or "how early can I detect a dead end."
def backtrack(state):
if is_complete(state):
record_solution(state)
return
for choice in next_choices(state):
if not can_place(state, choice): # the expensive, load-bearing check
continue
make_choice(state, choice)
backtrack(state)
undo_choice(state, choice) # mutation + undo, not just list append/popThe key mental shift from the previous two subtopics: state is no longer just a growing list you append to and pop from. It's a mutable shared structure — a chessboard, a Sudoku grid, a 2-D character grid — and "choose" and "un-choose" mean mutating and then precisely restoring specific cells of that structure.
Structure the search before you prune
Before optimizing is_valid, ask: what is one decision per recursion level?
Constraint problems are often secretly "assign a value to the next variable," not "pick any free cell / any free slot on the board." If the search is allowed to place pieces in any order among unordered positions, the same complete configuration is rediscovered through many construction paths — and the usual bandage is a seen set of finished boards or strings. That set is a smell: fix the branching so each solution has one canonical construction order, then you won't need to dedupe at the leaves.
The general move:
- Find an invariant that partitions the problem into an ordered sequence of variables (one queen per row; one digit per empty cell in a fixed scan order; the next IP octet among exactly four; the next character index in a target word).
- Make recursion depth = the next variable to assign.
- At that level, only try the domain of that variable (columns for this row; digits 1–9 for this cell; slice lengths 1–3 for this octet; the four neighbors for this letter).
- Encode constraints incrementally (sets, bitmasks, in-place markers) so
can_placeis O(1) — don't rescan the whole board on every try.
| Problem shape | Variable at depth d | Domain to try | What you stop doing |
|---|---|---|---|
| N-Queens | row d | columns 0..n-1 | picking any free cell in any order |
| Sudoku | next empty cell (fixed order) | digits 1..9 | trying every cell as "where to write next" |
| Restore IP Addresses | next octet among 4 | lengths 1..3 | placing dots in arbitrary index sets |
| Permutations | next position in the result | unused elements | generating the same ordering via different index sequences (→ duplicate-skip / used[]) |
| Word Search | next index in word | up to 4 neighbors | treating the board as an unordered bag of free cells |
This is the same instinct as the duplicate-handling rule in Permutations: force a canonical order among symmetric choices so the tree doesn't re-derive the same answer. Here the symmetry is spatial or positional ("which queen did I place first?") rather than equal values.
Rule of thumb: if your solution needs a hash set of completed configurations to uniquify the output, the decision variables are almost certainly wrong. Pruning inside can_place is still essential — but structuring the variables is the pruning that happens before the first recursive call.
N-Queens: state tracking without O(n) re-scans
The N-Queens problem places n queens on an n × n board so no two attack each other (same row, column, or diagonal). Apply the section above: two queens can never share a row, so the variable at depth r is "column for row r" — place exactly one queen per row and recurse row by row. That alone collapses the search from "choose among n² cells, then dedupe boards" down to "choose one column per row," which is already the first and most important pruning decision in the problem.
The naive is_valid check re-scans the whole board for conflicts on every placement — O(n) work per check, O(n) checks per row, O(n) rows, so O(n²) work just for validation on top of the O(n!)-shaped search tree. The standard optimization tracks three sets of "attacked" lines and updates them incrementally as you place/remove a queen, turning each validity check into O(1):
def solve_n_queens(n):
col = set()
pos_diag = set() # identified by (row + col) — constant along "/" diagonals
neg_diag = set() # identified by (row - col) — constant along "\" diagonals
res = []
board = [["."] * n for _ in range(n)]
def backtrack(row):
if row == n:
res.append(["".join(r) for r in board])
return
for c in range(n):
if c in col or (row + c) in pos_diag or (row - c) in neg_diag:
continue # O(1) pruning check
col.add(c); pos_diag.add(row + c); neg_diag.add(row - c)
board[row][c] = "Q"
backtrack(row + 1)
col.remove(c); pos_diag.remove(row + c); neg_diag.remove(row - c)
board[row][c] = "."
backtrack(0)
return resThe insight that every cell on the same "/" diagonal shares the same row + col value (and every cell on the same "" diagonal shares the same row - col value) is what makes O(1) diagonal-conflict checking possible — it's a small piece of coordinate geometry that turns into a huge constant-factor win. Every choice here touches five pieces of state (board, col, pos_diag, neg_diag, implicitly row), and every one of them must be undone on the way back up. Miss undoing one set and every subsequent branch inherits a phantom conflict — the search silently returns fewer solutions than exist, without crashing or throwing any error, which makes it a nasty bug to catch.
Sudoku and Word Search: board mutation + undo
Sudoku Solver and Word Search share a pattern: the "board" itself doubles as the visited/state tracker, so you mutate a cell directly, recurse, and then restore the cell's original value:
# Word Search: is `word` reachable starting from (r, c)?
def exist(board, word):
rows, cols = len(board), len(board[0])
def dfs(r, c, i):
if i == len(word):
return True
if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[i]:
return False
original = board[r][c]
board[r][c] = "#" # mutate: mark visited in place
found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1) or
dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
board[r][c] = original # undo: restore before returning
return found
return any(
dfs(r, c, 0)
for r in range(rows) for c in range(cols)
if board[r][c] == word[0]
)Using a sentinel character ("#") as the "visited" marker avoids allocating a separate visited set — O(1) extra space instead of O(word length) — but it only works because you reliably restore the original character on every exit path, including early return False paths that happen before your undo line if you're not careful with the control flow (the and/or short-circuit chain above deliberately runs the undo after all four recursive attempts, not interleaved with them).
Sudoku Solver follows the identical shape: find an empty cell, try digits 1–9, mutate the cell, recurse, and reset the cell to empty (.) if the recursive call fails. The pruning lever here is the is_valid(board, row, col, digit) check — the naive version rescans the row, column, and 3×3 box (27 cells) on every attempted digit; the optimized version maintains bitmasks or boolean arrays per row/column/box (the same "avoid O(n) re-scans" idea as N-Queens's diagonal sets) so each check becomes O(1) instead of O(1) work amortized over a fixed 27-cell scan.
Why pruning is the difference between milliseconds and a timeout
All three problems have a nominally exponential-or-worse search space:
- N-Queens: naively placing queens anywhere is
n²choosen; restricting to one per row drops it tonⁿ; adding column/diagonal pruning drops the actually visited tree to a small fraction ofn!. - Sudoku: naively, 81 cells × 9 digits is
9^81— astronomically impossible. Restricting to only empty cells and only valid digits per the row/column/box constraints is what makes the problem tractable at all. - Word Search: naively exploring all 4 directions from all
m × nstarting cells for a word of lengthLisO(m · n · 4^L)— pruning via the sentinel visited-marker (never revisit a cell in the same path) and the immediate character-mismatch check keeps real-world grids fast.
The recurring theme: the worst-case asymptotic complexity does not change — it's still exponential/factorial in the input size — but a cheap, correct is_valid/can_place check applied as early as possible (before recursing, not after) prunes entire subtrees in O(1), and in practice this is the difference between a solution that returns in milliseconds and one that times out. This is the same worst-case-vs-practical distinction from Subsets & Combinations and Permutations, just with the stakes much higher because these search trees are denser and the constraints tighter.
State representation comparison
| Problem | What "state" is mutated | Undo mechanism | Validity check cost (naive → optimized) |
|---|---|---|---|
| N-Queens | col, pos_diag, neg_diag sets + board | Remove from sets, reset board cell | O(n) rescan → O(1) set lookup |
| Sudoku Solver | Board cell + row/col/box trackers | Reset cell to ., clear tracker bits | O(27) rescan → O(1) bitmask check |
| Word Search | Board cell (sentinel char) or separate visited grid | Restore original character or unmark visited | O(1) inherently — cost is in branching (4 directions), not validity |
Complexity analysis
State the exponential/factorial classes precisely, the same way you would for Subsets & Combinations (O(2ⁿ)) and Permutations (O(n!)):
- N-Queens: worst-case O(n!) — one queen per row, decreasing valid columns per row due to pruning, structurally similar to a permutation search over columns.
- Sudoku Solver: worst-case O(9^m) where
mis the number of empty cells — bounded by trying up to 9 digits per empty cell. - Word Search: O(m · n · 4^L) where
Lis the word length — 4 directions per character, attempted from every starting cell.
As in the earlier subtopics, quote both the theoretical worst case and the practical effect of pruning when discussing these in an interview: "Worst case is O(n!), but column/diagonal pruning means we discard the vast majority of the tree before ever reaching depth n — in practice N-Queens for n=8 explores a few thousand nodes, not 8! ≈ 40,000, and far less than 8⁸ ≈ 16 million." Precision about which number you're citing (raw branching factor to the power of depth, vs. the tighter factorial bound after the "one per row" restriction, vs. actual pruned node count) is exactly the kind of rigor a Senior-level interviewer is listening for — the same instinct covered generally in the Problem-Solving Framework & Interview Communication topic.
Common pitfalls
- Searching an unordered placement space, then deduping finished solutions. Trying "any free cell" (or any free slot) at every step and stuffing complete boards/strings into a
seenset "fixes" uniqueness by brute force. Prefer an ordered variable sequence (one assignment per row / empty cell / octet / character index) so each solution is constructed once. - Checking validity after recursing instead of before. The entire point of pruning is to avoid descending into a doomed subtree. If you recurse first and check validity inside the base case, you've paid the full cost of the search anyway — the check must gate the recursive call, not follow it.
- Forgetting to undo a mutation before backtracking — the #1 backtracking bug, again, at higher stakes. In subsets/permutations, forgetting to undo corrupts some future output. Here, forgetting to remove a queen's column/diagonal markers, or forgetting to reset a Sudoku cell to
., or forgetting to restore a Word Search sentinel character, corrupts the shared board/state for every subsequent sibling branch in the entire remaining search — a much larger blast radius because the state is global and heavily reused, not a simple list. Earlyreturn Truepaths are a common place this slips — every exit must restore state (or you must structure control flow so undo always runs). - O(n) validity re-scans that silently dominate runtime. A correct-but-naive
isSafe/isValidfunction that rescans the row, column, diagonals, or 3×3 box from scratch on every call is a common reason an otherwise-correct backtracking solution times out. Encode constraints in incremental trackers (sets, bitmasks, boolean arrays) instead of rescanning. - Off-by-one errors in diagonal identification.
row + colandrow - colare the standard diagonal identifiers, butrow - colcan be negative — if you're using an array instead of a set/hash map forneg_diag, you need an offset (row - col + n) to keep the index non-negative. - Mutable shared board passed without cloning at the point of recording a solution. When you find a valid complete configuration (e.g., a Sudoku solution or an N-Queens board), you must copy/serialize it into the results at that moment — the same "copy, not reference" bug from Subsets & Combinations and Permutations applies here too, just applied to a 2-D board instead of a 1-D list.
Further Resources (Optional)
- GeeksforGeeks — N Queen ProblemArticle20m
- NeetCode — N-Queens (LC 51) Solution & ExplanationArticle20m
- NeetCode — Word Search (LC 79) Solution & ExplanationArticle15m
- GeeksforGeeks — Sudoku SolverArticle20m
- Wikipedia — Eight queens puzzleReference10m
- Peter Norvig — Solving Every Sudoku Puzzle Quickly (constraint propagation, forward checking, arc consistency)Reference45m
- Abdul Bari — N-Queens Problem using Backtracking (state space tree, video)Video14m
- GeeksforGeeks — Printing All N-Queens Solutions Using Bit-Masking (O(1) diagonal/column checks)Article20m
- University of Toronto CSC384 — Backtracking Search for CSPs: MRV, Forward Checking & Arc Consistency (lecture notes)Reference25m
- Book: The Algorithm Design Manual (Skiena, 3rd ed.) — Ch. 7 §7.1-7.4 "Backtracking" (pruning, N-Queens as a worked example; pp. 231-244)Book25m
Practice Problems
Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked
- Restore IP AddressesMedium!3/525m
- Word SearchMedium!!!3/530m
- N-Queens IIHard!3/530m
- N-QueensHard!4/540m
Optional Practice (Extra Reps)
For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.
- Sudoku SolverHard~5/550m
- Unique Paths IIIHard~4/535m
- Matchsticks to SquareMedium~4/530m
- Rat in a MazeGeeksforGeeks~2/520m
- Robot Room CleanerHardPremium~4/535m