COMPUTER SCIENCE FUNDAMENTALS SERIES

Backtracking
Algorithms

State space trees · N-Queens · Sudoku ·
Constraint satisfaction · Pruning · Branch and bound
Mid-level software engineer track  ·  20 slides
02

What Is Backtracking?

The paradigm

Backtracking is a systematic method for exploring all potential solutions by building candidates incrementally and abandoning a candidate as soon as it cannot lead to a valid solution.

Core idea

  • Build a solution one decision at a time
  • At each step, check constraints — if violated, prune this branch
  • If a dead end is reached, undo the last decision and try the next option
  • If all options exhausted at a level, backtrack further

When to use backtracking

🧩 Constraint satisfaction — Sudoku, crosswords, scheduling
🔀 Combinatorial search — permutations, combinations, subsets
Puzzle solving — N-Queens, Sudoku, mazes
📈 Graph problems — colouring, Hamiltonian paths
🎯 Optimisation — with branch and bound extension

Key insight: backtracking turns exponential brute force into something practical by cutting off invalid branches early. The earlier you prune, the faster the search.

03

State Space Trees

A state space tree is a rooted tree representing all possible states of a backtracking algorithm. Each node is a partial solution; edges represent decisions.

RootEmpty solution — no decisions made
Internal nodesPartial solutions with some decisions made
Leaf nodesComplete candidates — valid solutions or dead ends
Pruned branchesSubtrees skipped because constraints already violated

Without pruning: tree has bd leaves (b = branching factor, d = depth). With pruning: many subtrees are never explored.

{} +a -a {a} {} +b -b {a,b} {a} {b} {} a,b,c a,b a,c a b,c b c {} Binary subsets of {a, b, c} — each level: include or exclude one element Depth = decisions made 2^3 = 8 leaf nodes
04

The Backtracking Template

Generic pseudocode

def backtrack(state, decisions):
    if is_solution(state):
        record_solution(state)
        return

    for choice in get_choices(state, decisions):
        if is_valid(state, choice):     # prune
            apply(state, choice)        # choose
            backtrack(state, decisions)  # recurse
            undo(state, choice)         # unchoose

Pattern recognition: if a problem asks "find all configurations satisfying constraints" or "does a valid arrangement exist", backtracking is the first technique to consider.

The three pillars

is_valid() — Pruning

Rejects partial solutions that cannot lead to a valid complete solution. The earlier this fires, the more work is saved.

apply() / undo() — Choose / Unchoose

State mutation and reversal. The candidate is modified in place and restored after recursion — no copying needed.

is_solution() — Base Case

Recognises when a complete, valid solution has been built. Records or returns the result.

Recursive backtracking uses the call stack to store state. Iterative backtracking uses an explicit stack — identical complexity, avoids stack overflow for very deep searches.

05

N-Queens Problem

Problem statement

Place N queens on an N x N chessboard so that no two queens attack each other — no shared row, column, or diagonal.

Strategy

  • Place queens one row at a time (row 0, 1, ..., N-1)
  • For each row, try each column position
  • Check constraints: no column conflict, no diagonal conflict
  • If no valid column exists, backtrack to the previous row

Solution counts

NSolutionsUnique (symmetry)
421
89212
1214,2001,787
14365,59645,752

Constraint checking

def is_safe(board, row, col):
    for prev_row in range(row):
        prev_col = board[prev_row]
        # same column
        if prev_col == col:
            return False
        # same diagonal
        if abs(prev_col - col) == row - prev_row:
            return False
    return True
4-Queens solution: [1, 3, 0, 2]
06

N-Queens — Pruning in Action

4-Queens search trace

Row 0: col 0 ✓
  Row 1: col 0 ✗ (column)
  Row 1: col 1 ✗ (diagonal)
  Row 1: col 2 ✓
    Row 2: col 0 ✗ (diagonal)
    Row 2: col 1 ✗ (col conflict row 1)
    Row 2: col 2 ✗ (col conflict row 1)
    Row 2: col 3 ✗ (diagonal)
    ← backtrack to row 1
  Row 1: col 3 ✓
    Row 2: col 0 ✗ (diagonal)
    Row 2: col 1 ✓
      Row 3: col 0 ✗  col 1 ✗  col 2 ✗  col 3 ✗
      ← backtrack
    Row 2: col 2 ✗ ...
    ← backtrack to row 0
Row 0: col 1 ✓
  Row 1: col 3 ✓
    Row 2: col 0 ✓
      Row 3: col 2 ✓  ★ SOLUTION [1,3,0,2]

Efficiency

Brute force

Check all NN arrangements — 44 = 256 for N=4

Backtracking

Typically explores only a small fraction of the tree

Approach8-Queens nodes
Brute force (88)16,777,216
Backtracking~114
Speedup~147,000x

The power of pruning: each invalid placement at row r eliminates an entire subtree of N(N-r-1) nodes.

07

Sudoku Solver

Problem

Fill a 9x9 grid so every row, column, and 3x3 box contains digits 1–9 exactly once.

Backtracking approach

def solve_sudoku(board):
    cell = find_empty(board)
    if cell is None:
        return True           # solved

    row, col = cell
    for num in range(1, 10):
        if is_valid(board, row, col, num):
            board[row][col] = num
            if solve_sudoku(board):
                return True
            board[row][col] = 0   # undo

    return False  # trigger backtracking

Optimisations

Naked singles

If only one number is valid for a cell, place it immediately — no branching needed

Hidden singles

If a number can only go in one cell in a row/col/box, place it

Constraint propagation (AC-3)

Reduce domains before branching — eliminates options proactively

MRV heuristic

Fill the cell with fewest remaining options first — fail faster

Sudoku with constraint propagation solves most newspaper puzzles without any backtracking at all. Hard puzzles (17-clue minimum) still require search.

08

Subset Sum

Problem

Given a set S = {s1, s2, ..., sn} and a target T, find all subsets of S that sum to T.

Backtracking solution

def subset_sum(nums, target, start, curr, res):
    if target == 0:
        res.append(curr[:])
        return
    if target < 0:
        return                  # prune: overshot

    for i in range(start, len(nums)):
        curr.append(nums[i])
        subset_sum(nums, target - nums[i],
                   i + 1, curr, res)
        curr.pop()              # backtrack

Pruning strategies

Sort first

If nums[i] > remaining target, skip all subsequent elements — they are larger

Skip duplicates

If nums[i] == nums[i-1] at same recursion level, skip to avoid duplicate subsets

Running sum check

Maintain sum of unused elements; if remaining_sum < target, prune — impossible to reach target

Subset sum is NP-complete. Backtracking with pruning is practical for moderate inputs (n < ~40). For larger inputs, dynamic programming or meet-in-the-middle may be preferable.

09

Permutations

Generate all permutations of [1, 2, ..., n]

def permute(nums, start, result):
    if start == len(nums):
        result.append(nums[:])
        return

    for i in range(start, len(nums)):
        nums[start], nums[i] = nums[i], nums[start]
        permute(nums, start + 1, result)
        nums[start], nums[i] = nums[i], nums[start]

State space

  • Level 0: n choices for position 0
  • Level 1: n-1 choices for position 1
  • Total leaves: n! (all valid — no pruning for plain permutations)

Permutations with constraints

When constraints are added (e.g., "no two adjacent elements differ by more than 2"), pruning becomes effective:

  • Check the constraint at each level before recursing
  • Invalid placements prune entire subtrees
  • Transforms O(n!) into something much smaller in practice

Avoiding duplicates

For inputs with repeated elements (e.g., [1, 1, 2]):

  • Sort the input first
  • At each recursion level, skip element i if nums[i] == nums[i-1] and i-1 was not used at this level
10

Combinations

C(n, k): choose k elements from n

def combine(n, k, start, current, result):
    if len(current) == k:
        result.append(current[:])
        return

    # pruning: need (k - len(current)) more
    # only proceed if enough elements remain
    for i in range(start, n-(k-len(current))+2):
        current.append(i)
        combine(n, k, i + 1, current, result)
        current.pop()

Pruning the search space: at each level, if not enough remaining elements to fill the combination, stop immediately.

Combinations vs permutations

PropertyPermutationsCombinations
Order mattersYesNo
Countn! / (n-k)!n! / (k!(n-k)!)
Start indexFixed (swap)Advances
Typical pruningConstraintSize + constraint

Combinations are a strict subset of the permutation search space. Using the start index avoids generating [1,2] and [2,1] separately.

Without pruning: explore all 2n subsets, filter by size k. With pruning: only generate C(n,k) valid combinations directly.

11

Graph Colouring

Problem

Assign colours to vertices of an undirected graph such that no two adjacent vertices share the same colour, using at most k colours.

def graph_colour(graph, k, colours, vertex):
    if vertex == len(graph):
        return True             # all coloured

    for c in range(1, k + 1):
        if is_safe(graph, colours, vertex, c):
            colours[vertex] = c
            if graph_colour(graph, k, colours,
                            vertex + 1):
                return True
            colours[vertex] = 0  # backtrack

    return False

Applications

Map colouring

Four colour theorem guarantees k=4 suffices for planar graphs

Register allocation

Compilers assign variables to CPU registers (graph = interference graph)

Scheduling

Exams, tasks, or frequencies with conflict constraints

Timetabling

Courses sharing students cannot be in the same slot

The minimum k for which a valid colouring exists is the chromatic number X(G). Finding X(G) is NP-hard.

12

Hamiltonian Path & Cycle

Definitions

Hamiltonian pathVisits every vertex exactly once
Hamiltonian cycleA Hamiltonian path that returns to the starting vertex
def hamiltonian(graph, path, visited):
    if len(path) == len(graph):
        if graph[path[-1]][path[0]]:
            return True   # cycle found
        return False

    for v in range(len(graph)):
        if not visited[v] and graph[path[-1]][v]:
            visited[v] = True
            path.append(v)
            if hamiltonian(graph, path, visited):
                return True
            path.pop()
            visited[v] = False   # backtrack

    return False

Pruning strategies

Degree check

If an unvisited vertex has no unvisited neighbours, prune immediately

Connectivity check

If removing the current vertex disconnects the unvisited subgraph, prune

Warnsdorff's heuristic

Choose the vertex with fewest unvisited neighbours next — reduces branching factor

Hamiltonian cycle is NP-complete. No polynomial algorithm is known. Backtracking is the standard exact solver, but impractical for graphs with hundreds of vertices.

13

Constraint Satisfaction Problems

Formal definition

A CSP is defined by three components:

Variables

X1, X2, ..., Xn — the unknowns to assign

Domains

D1, D2, ..., Dn — possible values for each variable

Constraints

Restrictions on which combinations of values are allowed

Why CSP framing matters: provides a uniform representation — one algorithm solves many problems. Separates problem modelling from search strategy.

Examples as CSPs

ProblemVariablesDomains
N-QueensQueen per row{1..N}
SudokuEach cell{1..9}
Graph colourVertex colours{1..k}
Map colourRegion colours{R,G,B,Y}
SchedulingTask slotsAvailable slots

Constraints: N-Queens — no shared col/diag. Sudoku — row/col/box uniqueness. Graph colouring — adjacent vertices differ. Scheduling — no resource conflicts.

14

CSP Solving Strategies

Variable ordering

  • MRV — Minimum Remaining Values: choose the variable with the smallest domain; "fail-first" principle
  • Degree heuristic — choose the variable involved in the most constraints on unassigned variables

Value ordering

  • LCV — Least Constraining Value: choose the value that rules out the fewest options for neighbours
  • Maximises remaining flexibility in the search

Inference

  • Forward checking — after assignment, remove inconsistent values from neighbours
  • AC-3 — enforce arc consistency across all constraint arcs
  • MAC — run AC-3 after every assignment

Impact on search performance

StrategyEffect
Plain backtrackingExplores many dead-end branches
+ MRVDetects failures earlier — fail-first
+ Forward checkingPrunes domains proactively after each assignment
+ MAC (AC-3)Near-optimal pruning; solves most CSPs efficiently
15

Pruning Strategies

Without pruning, backtracking degenerates to brute force. Effective pruning is the difference between practical and intractable.

Types of pruning

Feasibility pruning

Reject partial solutions that already violate a constraint

Bound pruning

In optimisation, reject branches that cannot improve the best known solution

Symmetry breaking

Avoid exploring configurations that are rotations/reflections of already-explored solutions

Dominance pruning

Skip a partial solution if another is provably at least as good

Constraint propagation

Proactively reduce variable domains rather than waiting for conflicts

Implementation guidelines

  • Check constraints as early as possible — after each decision, not only at leaf nodes
  • Maintain incremental data structures — e.g., boolean arrays for used columns/diagonals in N-Queens
  • Sort choices — try the most constrained or promising option first to find solutions (or contradictions) sooner
  • Memoisation — cache results of subproblems when the state space has overlapping structure

Rule of thumb: the cost of the pruning check must be less than the cost of exploring the pruned subtree. Cheap checks that eliminate large subtrees are the sweet spot.

16

Optimisation Techniques

Bit manipulation

For problems with small state spaces (N-Queens with N ≤ 30), represent sets as bitmasks for O(1) constraint checks.

def solve(row, cols, diag1, diag2, n):
    if row == n:
        return 1
    count = 0
    available = ((1 << n) - 1) \
              & ~(cols | diag1 | diag2)
    while available:
        bit = available & (-available)
        count += solve(row + 1,
                       cols  | bit,
                       (diag1 | bit) << 1,
                       (diag2 | bit) >> 1, n)
        available ^= bit
    return count

Other techniques

Iterative deepening

Combine DFS with depth limits. Useful when solution depth is unknown and memory is constrained.

Randomised restarts

For hard CSPs, restart from a random initial state if the search stalls. Avoids unproductive subtrees.

Parallelism

Distribute independent subtrees across threads. Work stealing for load balancing. Near-linear speedup for many-branch problems.

17

Branch and Bound

Extending backtracking for optimisation

Branch and bound adds a bounding function that estimates the best possible solution achievable from a partial solution.

How it works

  • Branch — split the problem into subproblems (like backtracking)
  • Bound — compute an optimistic estimate for each subproblem
  • Prune — if the bound is worse than the best known solution, discard

Example: 0/1 Knapsack

  • Branch — for each item, include or exclude
  • Bound — use fractional knapsack (greedy) as upper bound
  • Prune — if upper bound ≤ current best profit, skip subtree

Backtracking vs branch and bound

AspectBacktrackingBranch & Bound
GoalFeasible solutionsOptimal solution
PruningConstraint violationsBound vs best-so-far
Bounding fnNot requiredEssential
ProblemsCSPs, enumerationTSP, knapsack, scheduling

Branch and bound is the standard approach for integer linear programming (ILP) solvers like CPLEX and Gurobi. The quality of the bounding function determines solver performance.

18

Backtracking vs Brute Force

Brute force

Generate all possible candidates, then check each for validity. Always explores the full search space. Time: O(bd).

Backtracking

Generate candidates incrementally, pruning invalid branches early. Explores only a fraction of the space. Worst case same; best case dramatically faster.

When backtracking does not help

  • All solutions are valid (plain enumeration) — no pruning opportunities
  • Constraints only checkable at leaf nodes — no early termination
  • Highly connected constraint graphs — pruning eliminates few branches

Empirical comparison

ProblemBrute ForceBacktrackingSpeedup
8-Queens16,777,216~114 nodes~147,000x
Sudoku (easy)951~100 nodesastronomical
Graph colour (sparse)kn~O(k*n)exponential

Backtracking is not a different complexity class — it is a constant-factor (often enormous) improvement within the same exponential class. For polynomial-time solutions, you need a fundamentally different algorithm.

19

Classic Problem Complexities

Time complexities

ProblemWorst CaseTypical with PruningNotes
N-QueensO(N!)Much less in practiceOne queen per row eliminates NN
SudokuO(981)Near-instant for mostConstraint propagation dominates
Subset SumO(2n)Depends on targetDP may be better for small targets
PermutationsO(n!)O(n!) unconstrainedNo pruning for plain case
Combinations C(n,k)O(C(n,k))O(C(n,k))Size pruning only
Graph ColouringO(kn)Problem-dependentSparse graphs prune well
Hamiltonian CycleO(n!)Problem-dependentNP-complete
TSP (B&B)O(n!)Manageable for n<25Bounding fn quality is key

Space complexity

Recursive backtracking: O(d) stack space where d = max recursion depth. Storing all solutions: O(S * d) where S = number of solutions.

Practical takeaway

Backtracking complexity is problem-instance dependent. Analyse the constraint structure, not just the worst case, to predict real-world performance.

20

Summary & Further Reading

Key takeaways

  • Backtracking is brute force with pruning — build candidates incrementally, abandon invalid branches early
  • The state space tree is the mental model: every node is a decision, pruning removes subtrees
  • The generic template — choose, check, recurse, unchoose — applies to N-Queens, Sudoku, graph colouring, and hundreds of other problems
  • CSPs unify many backtracking applications under one formal framework
  • Variable/value ordering (MRV, LCV) and constraint propagation (AC-3, MAC) dramatically reduce search effort
  • Pruning quality is the single biggest performance lever
  • Branch and bound extends backtracking to optimisation with bounding functions
  • Backtracking does not change the complexity class — but the constant factor is often the difference between seconds and centuries

Recommended reading

SourceDescription
Cormen et al.Introduction to Algorithms (CLRS) — backtracking and branch-and-bound
SkienaThe Algorithm Design Manual — practical strategies and war stories
Russell & NorvigAI: A Modern Approach — CSP chapter with AC-3, MRV, MAC
KnuthTAOCP Vol. 4 — exhaustive enumeration and Dancing Links
LeetCodeBacktracking tag — curated practice problems
SedgewickAlgorithms — permutations, combinations, constraint search