Constraint satisfaction

Forward Checking

Backtracking with a lookout: prune bad values before you trip over them.

Interactive demo coming soon β€” read the theory below

The idea

Forward Checking is plain backtracking search with an early-warning system. Every time you assign a value to a variable, you immediately delete any values from the still-unassigned neighbouring variables that now conflict with your choice. If any neighbour is left with no legal values at all, you know this path is doomed, so you backtrack right away instead of blundering deeper. It finds failure sooner and saves wasted work.

How it works

  1. Start with every variable unassigned and each variable holding its full domain (its list of allowed values).
  2. Pick an unassigned variable and choose a value from its current domain.
  3. Look at every unassigned variable connected to it by a constraint (its neighbours).
  4. From each neighbour's domain, remove any value that would clash with the value you just assigned.
  5. Check the neighbours: if any neighbour's domain is now empty, this assignment cannot work.
  6. If a domain went empty, undo the assignment (restore the removed values) and try the next value; if none are left, backtrack to the previous variable.
  7. If all domains still have at least one value, move on and assign the next variable.
  8. Repeat until every variable is assigned (success) or you run out of options at the very start (no solution).

OPEN, CLOSED & the data structures

A current assignment (which variable has which value so far), and a domain for each variable β€” the shrinking set of still-legal values. Forward Checking also keeps a record of which values it pruned at each step so it can restore them exactly when it backtracks (the classic recursive/stack structure of backtracking).

Pseudocode

function ForwardChecking(assignment, domains):
    if assignment is complete:
        return assignment                      # every variable has a value

    var = pick an unassigned variable
    for each value in domains[var]:
        assign var = value
        pruned = {}                            # remember what we remove
        empty = false
        for each unassigned neighbour Y of var:
            for each v in domains[Y] that conflicts with value:
                remove v from domains[Y]
                record v in pruned[Y]
            if domains[Y] is empty:
                empty = true                   # a neighbour has no options
        if not empty:
            result = ForwardChecking(assignment, domains)
            if result != failure:
                return result
        restore all values in pruned to their domains   # undo
        unassign var
    return failure                             # no value worked -> backtrack

At a glance

Complete?Complete β€” like backtracking, it systematically explores every possibility and will find a valid assignment if one exists, or report failure if none does. Pruning only removes values that provably cannot be part of any solution, so nothing valid is ever lost.
Optimal?For CSPs the goal is a valid, complete assignment (all constraints satisfied), and Forward Checking reliably finds one when it exists. Plain CSPs have no notion of a "best" solution, so optimality does not apply unless you add costs β€” Forward Checking simply guarantees a correct, complete assignment.
TimeO(b^m) worst case, same as backtracking (b = domain size, m = number of variables), but usually far fewer nodes are explored because dead ends are caught early.
SpaceO(mΒ·b) β€” it stores the current domains for all variables plus the pruned values needed to undo each step; linear in the number of variables.

πŸ‘ When to use it

  • Constraint problems like map colouring, Sudoku, N-Queens, or timetabling/scheduling
  • When plain backtracking wastes time exploring branches that were doomed from an early bad choice
  • When constraints are mostly between pairs of variables, so a neighbour's options are easy to prune
  • As a light, cheap upgrade to backtracking before reaching for heavier methods like full constraint propagation (AC-3)

⚠️ Watch out for

  • It only checks the direct neighbours of the just-assigned variable β€” it does NOT propagate further, so some doomed states slip through until later (that is what stronger methods like arc consistency / AC-3 catch).
  • You must restore the pruned values exactly when backtracking, or later branches will be missing legal options and you may wrongly report no solution.
  • It is still exponential in the worst case β€” it prunes, it does not magically make the problem easy.
  • Forgetting that a value was removed by an earlier assignment (not the current one) leads to restore bugs β€” track pruning per assignment step.
  • Pairs it best with smart heuristics (e.g. assigning the most-constrained variable first); on its own the ordering can still matter a lot.