leetcode / Backtracking / Constrained backtracking advanced

Constrained backtracking

The generate-everything search from the previous page is exponential, and most of it is wasted. If a half-built answer already breaks a rule, no way of finishing it can succeed, so the search should abandon it on the spot. Constrained backtracking is the same choose-explore-unchoose loop with one extra question before each choice: does this still have a chance? Asking it cheaply and early is where all the speed comes from.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant
  5. Keeping the check cheap
  6. Where the cuts come from
  7. Worked example
  8. Combination sum
  9. N-Queens
  10. Word search and palindrome cuts
  11. Implementation
  12. Variations and edge cases
  13. Complexity and tradeoffs
  14. Problems
  15. Medium
  16. Hard
  17. Also uses this pattern
  18. Connections
  19. Read first
  20. Read next
  21. Check yourself
  22. References

Problem

You must find one, all, or the number of arrangements that satisfy rules: numbers that add to a target, queens that do not attack each other, a path of letters through a grid, a string cut into palindromes. Checking a finished answer is easy; the trouble is the astronomical number of candidates. The rules, however, usually become violated early, long before the answer is complete, and that is the opening.

SignalPatternWhy
"Combinations that add up to a target"Sorted candidates, a remaining sum, break when a candidate overshootsEverything after an overshooting candidate in sorted order overshoots too.
"Place pieces so none conflict" (queens, sudoku)Place one piece per step, keep sets of used columns, rows or diagonalsA conflict test in constant time rejects a square before any recursion.
"Does this word exist in the grid"DFS with a visited mark that is undone on returnA cell may be reused by a different path, but never twice in one path.
"Partition so that every piece satisfies a property"Extend a prefix only with a valid next pieceAn invalid piece makes the whole branch dead.
"Split into k groups of equal sum"Fill one bucket at a time, prune on overshoot and on repeated bucket statesMany orderings of the same items lead to the same state.
"Count or optimize, with overlapping subproblems"Dynamic programming or memoizationIf the remaining problem repeats, cache it instead of searching it again.

Intuition

Walking a maze, you do not walk to the end of every corridor to learn it is a dead end. You turn around the moment you see the wall. Backtracking with constraints is the same: you test each next step against the rules, and a step that breaks a rule is never taken, which removes the entire subtree behind it with one comparison.

The power of that cut depends on how early it happens. Rejecting a bad queen on the second row saves every placement of rows three to eight; rejecting a bad grid letter on the first mismatch saves every longer path. So the craft is in ordering the choices and choosing the constraint test so that failures show up as high in the tree as they can.

The 4-queens search tree A tree of safe partial placements for 4 queens, one row per level. Each circle shows the column of the queen placed in that row. The tree has 17 nodes and 2 complete solutions, which are the leaves at the bottom level: columns 1, 3, 0, 2 and 2, 0, 3, 1 from top row to bottom row. Every other leaf is a dead end where the next row has no safe square. ∅ 0 2 3 1 1 3 0 2 2 0 3 1 3 0 2 1

Only safe placements become nodes. Of the 44 = 256 ways to put one queen in each row, the search ever builds 17 partial boards. Green leaves are solutions; the grey leaves higher up are dead ends, found one row early. The diagram scrolls sideways on a narrow screen.

Mechanics

The invariant

At every call, the path so far satisfies all the rules that can be checked on a partial answer. Each recursive step adds one choice that is validated before it is made, so the invariant holds on entry to the child. When the path is complete, it is therefore a valid answer; when no legal choice remains, the call returns and the loop in the parent tries its next option. Nothing valid is lost, because a rejected choice would have broken a rule that no extension could repair.

That last clause is the correctness condition for any pruning rule: it must only reject a branch when no completion of it is valid. A rule that rejects too much loses answers; a rule that rejects too little is merely slow.

Keeping the check cheap

The test runs at every node, so it must not scan the board. Keep the facts in sets that you update on choose and restore on unchoose:

  • For N-Queens, three sets: used columns, used row - col diagonals and used row + col anti-diagonals. A square is safe if all three miss. One queen per row is guaranteed by recursing one row at a time.
  • For combination sum, the remaining target. Sorting the candidates once means the first candidate that exceeds the remainder ends the loop with a break, not a continue.
  • For word search, the current index into the word, and a visited mark written into the grid itself and restored on the way out.

Where the cuts come from

Three kinds of pruning cover almost every problem. Feasibility: reject a choice that already violates a rule (a queen under attack, a letter that does not match). Bounds: reject when even the best case cannot succeed (the remaining sum is negative, too few items are left). Symmetry and duplicates: do not explore two branches that lead to the same state (a start index so combinations are not reordered, a sort and skip for equal values).

A word search path through a letter grid A grid of 3 rows and 4 columns. The word ABCCED is found by starting at the top-left A and moving through 6 adjacent cells, each used once. The path cells are numbered in the order they are visited. A 1 B 2 C 3 E S F C 4 S A D 6 E 5 E green: cells on the path, small number: step

The path may bend but never reuses a cell, so each cell is marked while it is on the path and restored when the search backs out.

Worked example

Combination sum

Candidates 2, 3, 6, 7 (each usable any number of times), target 7. Sorted candidates plus the early break find 2 answers (2+2+3, 7) in 10 calls. The loop examines 16 candidates; without the break, which would skip each overshoot one by one, it would examine 27.

The no-reuse variant on 10, 1, 2, 7, 6, 1, 5 with target 8 sorts to 1, 1, 2, 5, 6, 7, 10 and uses the duplicate-skip rule from the previous page. It finds 4 distinct answers: 1+1+6, 1+2+5, 1+7, 2+6.

N-Queens

For 4 queens the search tries 60 squares in total, builds 17 safe partial boards and finds 2 solutions. Written as the column of the queen in each row from the top, they are:

SolutionColumns, top row to bottom
11, 3, 0, 2
22, 0, 3, 1

Scaling up: 6 queens has 4 solutions and 8 queens has 92 solutions, yet the search needs only 2057 nodes for 8 queens, instead of the 88 = 16,777,216 boards of one queen per row.

Word search and palindrome cuts

Searching the grid for ABCCED succeeds after 9 calls along a 6-cell path. Searching for ABCB fails after 20 calls: it walks as far as ABC, then the third letter's neighbors cannot supply another B without reusing a cell.

Cutting "aab" into palindromes gives 2 ways (a|a|b, aa|b) in 6 calls. The piece "ab" is never extended, because it is not a palindrome.

Implementation

def combination_sum(candidates, target):
    nums = sorted(candidates)
    results, path = [], []

    def explore(start, remaining):
        if remaining == 0:
            results.append(path[:])
            return
        for i in range(start, len(nums)):
            if nums[i] > remaining:
                break                          # sorted: every later candidate overshoots too
            path.append(nums[i])
            explore(i, remaining - nums[i])    # i, not i + 1: the same item may repeat
            path.pop()

    explore(0, target)
    return results


def solve_n_queens(n):
    cols, diag, anti = set(), set(), set()
    placed, boards = [], []

    def explore(row):
        if row == n:
            boards.append(["." * c + "Q" + "." * (n - c - 1) for c in placed])
            return
        for col in range(n):
            if col in cols or row - col in diag or row + col in anti:
                continue
            cols.add(col); diag.add(row - col); anti.add(row + col)
            placed.append(col)
            explore(row + 1)
            placed.pop()
            cols.remove(col); diag.remove(row - col); anti.remove(row + col)

    explore(0)
    return boards


def exist(board, word):
    rows, cols = len(board), len(board[0])

    def explore(r, c, i):
        if board[r][c] != word[i]:
            return False
        if i == len(word) - 1:
            return True
        saved, board[r][c] = board[r][c], "#"
        found = any(
            0 <= nr < rows and 0 <= nc < cols and explore(nr, nc, i + 1)
            for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1))
        )
        board[r][c] = saved
        return found

    return any(explore(r, c, 0) for r in range(rows) for c in range(cols))

Variations and edge cases

def partition_palindromes(s):
    results, path = [], []

    def explore(start):
        if start == len(s):
            results.append(path[:])
            return
        for end in range(start + 1, len(s) + 1):
            piece = s[start:end]
            if piece == piece[::-1]:
                path.append(piece)
                explore(end)
                path.pop()

    explore(0)
    return results
  • Sudoku. The same template with one set per row, column and box. Always fill the empty cell with the fewest legal digits first; that ordering often turns a hard puzzle into a quick one.
  • Equal-sum partitions (Matchsticks, k equal subsets). Sort descending so big items fail fast, reject early if the total is not divisible by k, and skip trying an empty bucket twice, because two empty buckets are interchangeable.
  • Word Break II. The prefix-extension shape of the palindrome code, with the dictionary lookup as the test. Memoize the answers for each suffix when words overlap heavily, or the search repeats itself.
  • Marks that leak. Restoring the grid cell must happen on every exit path, including the early success return, or later searches see a damaged grid. Using any(...) with a restore after it keeps the exits to one.
  • Break versus continue. break is only right when the candidates are sorted and the failing test is monotonic. Otherwise use continue.
  • Edge cases: a target of zero (the empty combination), no candidate small enough, a 1-cell grid, a word longer than the grid, an N that has no solution (2 and 3 queens) and a string that is already a palindrome.

Complexity and tradeoffs

  • Time: still exponential in the worst case: up to n! for queens and 4L for a word of length L on a grid. Pruning does not change the bound; it shrinks the constant enormously, and the real cost is the number of nodes that survive the checks.
  • Space: O(depth) for the call stack and the path, plus the constraint sets.
  • Baseline. Generate every candidate and filter at the end. For 8 queens that is millions of boards against 2057 nodes.
  • When it fails. If the same subproblem recurs (the same remaining sum with the same suffix of candidates, the same string suffix), memoizing it, or turning the search into a table, beats backtracking. If the question asks for a count or an optimum rather than the arrangements, dynamic programming is usually the intended tool. If a bad ordering makes the first branches hopeless, reorder so the most constrained choice comes first.

Problems

Every home problem has a rule that fails early. Combination Sum and Combination Sum II are the cleanest first exercises; N-Queens and Sudoku train the constant-time constraint state; Word Search trains reversible marking. Word Search II (listed below) pairs this with a trie so that many words share one search.

Medium

  • 39. Combination Sum Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Let each branch reuse the current candidate or move past it, and prune once the running sum passes the target.
  • 40. Combination Sum II NeetCode 150 NeetCode 250
    Sort first, use each number at most once, and skip equal values at the same depth so identical combinations never repeat.
  • 79. Word Search Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Depth-first search from each cell, marking cells on the current path so the word never reuses one, and unmark on return.
  • 131. Palindrome Partitioning NeetCode 150 NeetCode 250
    Cut off a palindromic prefix and recurse on the rest; precomputing which substrings are palindromes makes each check constant time.
  • 473. Matchsticks to Square NeetCode 250
    Sort the sticks longest first and place each into one of four sides, pruning sides that overflow and sides of equal length that were already tried.
  • 698. Partition to K Equal Sum Subsets NeetCode 250
    Fill k buckets of the target sum with numbers sorted descending, skipping buckets with the same current load to cut symmetric branches.

Hard

  • 37. Sudoku Solver Grind 169
    Fill empty cells one at a time with digits allowed by the row, column and box sets, and undo the moment a cell has no legal digit.
  • 51. N-Queens Grind 169 NeetCode 150 NeetCode 250
    Place one queen per row and track occupied columns and both diagonals in sets, so every placement check is constant time.
  • 52. N-Queens II NeetCode 250
    Run the row-by-row queen placement but only count complete boards, using column and diagonal sets to prune.
  • 140. Word Break II NeetCode 250
    Recurse on every dictionary word that starts the remaining string and memoize the sentence lists for each suffix.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

This page sharpens the template from subsets and permutations with a validity test and a bound, and it is DFS on a grid in the sense of the tree pages. Graph traversal drops the "unchoose" step because a visited set is permanent there, while here the mark is undone so another path may use the cell. When states repeat, dynamic programming is the next move.

Check yourself

Why is break correct in combination sum but wrong if the candidates are not sorted?

With sorted candidates, once nums[i] exceeds the remaining sum, every later candidate is at least as large and also exceeds it. Unsorted, a later candidate could be small enough, so break would lose answers; you would have to continue. Here it cut the candidates examined from 27 to 16.

What do the three sets in N-Queens test?

Whether the column is taken, and whether the two diagonals through the square are taken. Squares on one diagonal share row - col, and on the other share row + col, so each test is a constant-time set lookup. Rows need no set because the recursion places exactly one queen per row.

What goes wrong if word search forgets to restore the cell on a failed path?

The cell stays marked as visited, so another branch that legitimately needs it sees a wall and the search can report "not found" for a word that is in the grid. Every exit must undo the mark.

The 4-queens search built 17 nodes. Why not 256?

256 counts every way to put a queen in each row. The search only builds a node when the new queen is safe, and it never extends a board that already has a conflict, so whole subtrees of the 256 are never visited.

References

  • Skiena, The Algorithm Design Manual, chapter 9, backtracking with pruning, including the eight-queens and sudoku examples.
  • Knuth, The Art of Computer Programming, volume 4B, section 7.2.2 (backtrack programming), and Knuth's "Dancing Links" paper for exact-cover problems such as sudoku.
  • Russell and Norvig, Artificial Intelligence: A Modern Approach, chapter 6 on constraint satisfaction, including most-constrained-variable ordering.
  • Sedgewick and Wayne, Computer Science: An Interdisciplinary Approach, section 2.3 (recursion), whose exercises build permutations, combinations and the n-queens search.
  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 34 (NP-completeness): why exact search for problems such as satisfiability has no known polynomial algorithm, so backtracking with pruning stays exponential in the worst case.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.