Constraint Satisfaction Problems
After this page you can model a problem as a CSP, run backtracking search by hand, filter with forward checking and AC-3, and explain exactly why MRV and LCV pull in opposite directions.
How to model a problem as a CSP and solve it: backtracking search, plus filtering and ordering so that you discover you are doomed as early as possible.
- CSP — a triple $(X, D, C)$: variables, domains, constraints over subsets of the variables. A solution is a complete, consistent assignment. Every solution sits at depth $n$, so path cost and distance heuristics are meaningless.
- Search space — $d^{\,n}$ complete assignments, $O(d^{\,n})$ to enumerate; deciding satisfiability is NP-hard, since SAT is a Boolean CSP ($d = 2$).
- Backtracking search $=$ DFS $+$ one variable at a time (kills the $n!$ duplicate orderings: $n!\,d^{\,n}$ leaves $\to d^{\,n}$) $+$ fail-on-violation, the incremental goal test. Solves n-queens to $n \approx 25$.
- Forward checking — after $X = c$, delete $c$ from the domain of every unassigned neighbour; fail when a domain empties. Cost $O(\deg \cdot d)$. Blind spot: it never compares two unassigned variables.
- Arc consistency — the arc $X \to Y$ is consistent iff every value in the tail $D_X$ has at least one support in the head $D_Y$. Enforcing it deletes from the tail; the head is only read.
- AC-3 — queue of arcs; when a value dies in $D_{X_i}$, re-enqueue $(X_k, X_i)$ for every neighbour $X_k$ — the arcs into the shrunken domain. Cost $O(n^2 d^3)$, from $O(nd)$ deletions $\times$ $\le n$ arcs enqueued each $\times$ $O(d^2)$ per REVISE. Strictly stronger than forward checking: never fails later, sometimes earlier.
- Limitation — arc consistency is a two-variable property. An arc-consistent CSP with every domain non-empty may have no solution (three variables, pairwise $\neq$, two colours). The search decides satisfiability; AC-3 only prunes.
- MRV — assign the unassigned variable with the fewest values left, tie-broken by degree. Variables are an AND: all must be satisfied, so fail fast.
- LCV — try the value that rules out the fewest options for the neighbours. Values are an OR: only one must work, so succeed fast. With filtering, these make 1000-queens feasible.
Everything below is the explanation and the practice. If you are short on time, read this box, then go to the problems.
1. Planning problems vs. identification problems
Every search problem so far — BFS, DFS, UCS and A* — was a planning problem: you knew the goal and were asked how to get there, so the answer is a sequence of actions. Paths have costs (there is a best one) and depths (a shallow goal is cheaper, which is why BFS is complete and DFS is not), and a heuristic $h(n)$ is useful because it estimates remaining distance.
In an identification problem the answer is an assignment of values to variables, and nobody cares how you found it — like a completed crossword, where the order in which you wrote the letters is not part of the answer.
Planning: the path to the goal is the important thing. Paths have various costs and depths; heuristics give problem-specific guidance.
Identification: the goal itself is the important thing, not the path. Every solution sits at exactly the same depth — which is why CSPs get their own algorithms.
| Property | Planning (Ch. 1–3) | Identification (CSPs) |
|---|---|---|
| What you return | a sequence of actions | a complete assignment $\{X_1 = v_1, \dots, X_n = v_n\}$ |
| Depth of solutions | varies; shallow is better | always exactly $n$ (one assignment per variable) |
| Path cost | meaningful; we optimise it | meaningless — all solutions are equally good |
| Heuristics | estimate distance-to-go | "distance" is constant, so distance heuristics are useless |
| Useful guidance instead | $h(n)$, admissibility, consistency | which variable to fill next, which value to try, what becomes impossible |
That last row is the whole chapter. Because every solution is at depth $n$, a heuristic saying "you're getting closer" tells us nothing — on every branch we are exactly $n - |\text{assigned}|$ steps from completion. In planning you ask "am I close?"; in identification you ask "am I doomed?", and every algorithm below is a device for discovering that you are doomed as early as possible, so you can stop digging.
2. What exactly is a CSP?
In a standard search problem a state is an arbitrary data structure and the goal test is any function over states. That generality is why generic search can do nothing clever — it cannot look inside either one. A CSP gives up the generality on purpose and gets algorithms in exchange.
A constraint satisfaction problem is a triple $(X, D, C)$:
- Variables $X = \{X_1, \dots, X_n\}$. A state is an assignment of values to these variables.
- Domains $D$: the values a variable may take. Often one shared domain of size $d$, but in general each variable has its own $D_i$ — the course writes "sometimes $D$ depends on $i$". We write $D_i$ for the values still available to $X_i$.
- Constraints $C$: the goal test, written as a set of constraints, each specifying the allowable combinations of values for some subset of the variables.
A solution is a complete assignment (every variable has a value) that is consistent (every constraint is satisfied).
The clause that does all the work is "for subsets of the variables". A general goal test is a black box over the whole state, so every variable must be filled before you may call it. A constraint mentions two or three variables, so it can be evaluated on a partial assignment the instant those few are filled. Early failure detection is the entire performance story of this chapter.
3. Why bother? Where CSPs show up
An enormous number of real problems are, once you squint, "assign values to variables so that a pile of local rules all hold". The course's list:
Assignment: who teaches what class. Variables = courses, domain = faculty, constraints = nobody teaches two things at once, everybody qualified, nobody over their load.
Timetabling: variables = courses, domain = (time slot, room) pairs, constraints = no room double-booked, required courses not in conflict, room big enough.
Hardware configuration: components whose interfaces, power budgets and form factors are compatible. Transportation and factory scheduling: variables are job start times; B cannot start before A ends, two jobs cannot share a machine. Circuit layout: place and route without overlaps. Fault diagnosis: variables are "is component $i$ broken?", constraints are the observed symptoms.
Some instances involve real-valued variables — a start time is a real number, not a colour — which §8 returns to.
4. The running example: colouring Australia
The whole lecture runs on one instance, so it is worth setting up carefully: colour the seven regions of Australia so that no two regions sharing a border get the same colour, using only three colours.
Variables: $\text{WA},\ \text{NT},\ \text{Q},\ \text{NSW},\ \text{V},\ \text{SA},\ \text{T}$ — Western Australia, Northern Territory, Queensland, New South Wales, Victoria, South Australia, Tasmania. So $n = 7$.
Domains: $D_i = \{\text{red},\ \text{green},\ \text{blue}\}$ for every $i$, so $d = 3$.
Constraints: adjacent regions must have different colours. Written implicitly this is one inequality per shared border, e.g. $\text{WA} \neq \text{NT}$; written explicitly it is a list of allowed pairs, e.g. $(\text{WA}, \text{NT}) \in \{(\text{red},\text{green}),(\text{red},\text{blue}),(\text{green},\text{red}),\dots\}$ — all six ordered pairs of distinct colours.
Solutions are assignments satisfying all of them, for example $$\{\text{WA}=\text{red},\ \text{NT}=\text{green},\ \text{SA}=\text{blue},\ \text{Q}=\text{red},\ \text{NSW}=\text{green},\ \text{V}=\text{red},\ \text{T}=\text{green}\}.$$
The nine borders — memorise these, every worked example below uses them:
WA–NT WA–SA NT–SA NT–Q SA–Q SA–NSW SA–V Q–NSW NSW–V
Tasmania borders nothing.
Verify that solution border by border: WA(red)≠NT(green) ✓, WA(red)≠SA(blue) ✓, NT(green)≠SA(blue) ✓, NT(green)≠Q(red) ✓, SA(blue)≠Q(red) ✓, SA(blue)≠NSW(green) ✓, SA(blue)≠V(red) ✓, Q(red)≠NSW(green) ✓, NSW(green)≠V(red) ✓. All nine hold. T is unconstrained, so any of its three colours works — our first hint about structure.
5. Constraint graphs
A CSP is binary if each constraint relates at most two variables.
For a binary CSP, the binary constraint graph has one node per variable and one edge (arc) per constraint: an edge between $X$ and $Y$ means "there is a constraint restricting which pairs $(x,y)$ are allowed".
The right-hand picture above is such a graph, and it is what the general-purpose algorithms operate on. Note what it throws away: it records that WA and NT are constrained, not how. That is the point — an algorithm written against the graph works for map colouring, scheduling and n-queens alike, because it needs a constraint's scope, never its semantics.
Three ways graph structure speeds up search:
- Connected components. Tasmania is isolated, so it is an independent subproblem: components of sizes $n_1, n_2$ solved separately cost $O(d^{n_1}) + O(d^{n_2})$ instead of $O(d^{n_1 + n_2})$ — for Australia, $3^6 + 3^1 = 732$ instead of $3^7 = 2187$.
- Degree. A high-degree node (SA, degree 5) constrains many neighbours, so assigning it early prunes a lot — the "degree heuristic", MRV's tie-breaker.
- Global shape. If the graph is a tree, the CSP is solvable in $O(n d^2)$ with no backtracking. beyond syllabus See §18.
The constraint graph is not the search tree. The graph has 7 nodes and never changes during the search. The search tree has thousands of nodes and is built and destroyed as the algorithm runs. Confusing them is the most common source of "wait, which thing has depth $n$?" — the search tree has depth $n$; the graph has no depth.
alldiff mentions nine variables, so it is a higher-order constraint. (Domain size is $d$; it has nothing to do with arity.)6. N-queens, modelled twice
The lecture puts up two formulations of n-queens and leaves both blank. That blank is the single most useful exercise in the chapter, because it shows that modelling choices are algorithmic choices: the same puzzle, written down two ways, differs by a factor of $10^{12}$ in search-space size before any clever algorithm is applied.
The puzzle: place $n$ queens on an $n \times n$ chessboard so that no two attack each other — no shared row, column or diagonal.
Formulation 1 — one Boolean per square
Variables: $X_{ij}$ for $i, j \in \{1,\dots,n\}$ — one variable per square of the board. $i$ is the column, $j$ is the row. There are $n^2$ variables.
Domains: $X_{ij} \in \{0, 1\}$, where $1$ means "there is a queen on this square" and $0$ means "empty". So $d = 2$ and this is a Boolean CSP.
Constraints: four families, written out in full below.
Write the constraints explicitly. For every pair of squares that attack each other, we forbid the combination "both are 1". The allowed pairs are therefore $\{(0,0),(0,1),(1,0)\}$ — everything except $(1,1)$:
| Family | Constraint | Reads as | How many |
|---|---|---|---|
| Rows | $\forall i, j, k:\ (X_{ij},\, X_{kj}) \in \{(0,0),(0,1),(1,0)\}$ | no two queens in row $j$ | $n \cdot \binom{n}{2}$ |
| Columns | $\forall i, j, k:\ (X_{ij},\, X_{ik}) \in \{(0,0),(0,1),(1,0)\}$ | no two queens in column $i$ | $n \cdot \binom{n}{2}$ |
| Diagonal ↘ | $\forall i, j, k:\ (X_{ij},\, X_{i+k,\,j+k}) \in \{(0,0),(0,1),(1,0)\}$ | no two on a down-right diagonal | $O(n^3)$ |
| Diagonal ↗ | $\forall i, j, k:\ (X_{ij},\, X_{i+k,\,j-k}) \in \{(0,0),(0,1),(1,0)\}$ | no two on an up-right diagonal | $O(n^3)$ |
| Count | $\displaystyle\sum_{i,j} X_{ij} = n$ | exactly $n$ queens are placed | 1 (but it touches all $n^2$ variables) |
Indices stay on the board ($1 \le i+k \le n$ and so on). The first four families are binary, so they fit the constraint-graph picture of §5; the fifth does not — the count constraint mentions all $n^2$ variables at once. That global constraint is the ugly part of this model.
Forgetting the count constraint. Without it, $X_{ij} = 0$ for every square satisfies every attack constraint — an empty board is a perfectly legal "solution". The attack constraints only say not too many queens; something has to say enough queens. This is a general lesson: a CSP whose constraints are all "forbid" constraints usually has a trivial degenerate solution, and you must add the constraint that rules it out.
Size of the search space. There are $n^2$ variables each with $2$ values, so the number of complete assignments is
$$|{\text{assignments}}| = 2^{n^2}.$$For $n = 8$ that is $2^{64} \approx 1.845 \times 10^{19}$. If you could test a billion assignments per second you would need about 585 years.
Formulation 2 — one variable per column
Use a modelling trick that appears everywhere in CSPs: build a constraint into the variables themselves. Exactly one queen goes in each column (two would share the column; if some column had none then with $n$ queens in $n$ columns another has two). So instead of "is there a queen here?", ask "which row holds column $k$'s queen?".
Variables: $Q_1, Q_2, \dots, Q_n$ — one per column. $n$ variables.
Domains: $Q_k \in \{1, 2, \dots, n\}$, the row index of the queen in column $k$. So $d = n$.
Constraints, implicit form: $$\forall\, i \neq j: \quad Q_i \neq Q_j \quad\text{(no shared row)} \qquad\text{and}\qquad |Q_i - Q_j| \neq |i - j| \quad\text{(no shared diagonal)}.$$
Constraints, explicit form: for each pair $(i,j)$, the list of allowed value-pairs. For $n = 4$, for instance, $$(Q_1, Q_2) \in \{(1,3),\,(1,4),\,(2,4),\,(3,1),\,(4,1),\,(4,2)\}.$$
The diagonal condition is the one people mis-derive. Two queens sit at $(i, Q_i)$ and $(j, Q_j)$ in (column, row) coordinates, and they share a diagonal exactly when the horizontal distance equals the vertical distance, $|i - j| = |Q_i - Q_j|$. Forbidding that gives $|Q_i - Q_j| \neq |i-j|$. One absolute-value equation covers both diagonal directions, which is why formulation 2 needs one condition where formulation 1 needed two families.
Verify the explicit list for $n=4$, which the course writes with a "…" and moves on. We need $Q_1 \neq Q_2$ and $|Q_1 - Q_2| \neq |1-2| = 1$ — the two rows must differ by at least $2$:
| $Q_1 \backslash Q_2$ | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 1 | ✗ same row | ✗ diagonal | ✓ | ✓ |
| 2 | ✗ diagonal | ✗ same row | ✗ diagonal | ✓ |
| 3 | ✓ | ✗ diagonal | ✗ same row | ✗ diagonal |
| 4 | ✓ | ✓ | ✗ diagonal | ✗ same row |
Six allowed pairs out of sixteen: $(1,3), (1,4), (2,4), (3,1), (4,1), (4,2)$ — exactly the set above. That was one of the $\binom{4}{2} = 6$ constraints; for $n = 8$ you would fill $\binom{8}{2} = 28$ tables of $64$ cells. The implicit form says the same thing in two lines of algebra and costs two instructions to test instead of a table lookup.
Implicit constraint: a formula or piece of code that returns true/false for a tuple of values. Compact, but opaque — the solver cannot look inside it.
Explicit constraint: an enumerated table of allowed tuples. Bulky, but completely transparent — a solver can index it, intersect it, and precompute supports. Real solvers convert small implicit constraints into explicit tables for exactly that reason.
Why formulation 2 wins
Count complete assignments again. Formulation 2 has $n$ variables with $n$ values each:
$$|\text{assignments}| = n^n \qquad\text{versus}\qquad 2^{n^2}.$$| $n$ | Formulation 1: $2^{n^2}$ | Formulation 2: $n^n$ | Ratio |
|---|---|---|---|
| 4 | $2^{16} = 65{,}536$ | $4^4 = 256$ | $256\times$ |
| 8 | $2^{64} \approx 1.845 \times 10^{19}$ | $8^8 = 16{,}777{,}216$ | $\approx 1.1 \times 10^{12}\times$ |
| 16 | $2^{256} \approx 1.2 \times 10^{77}$ | $16^{16} \approx 1.8 \times 10^{19}$ | $\approx 6 \times 10^{57}\times$ |
For $n = 8$: 16.8 million assignments instead of 18.4 quintillion. Three further things improve, and they matter more than the raw count:
- Two constraint families vanish. "One queen per column" and "exactly $n$ queens" are now impossible to violate, and a constraint you cannot violate costs nothing to enforce. The best way to satisfy a constraint is to choose variables that cannot express its violation.
- The CSP becomes purely binary. The $n^2$-ary count constraint is gone, so the constraint graph is a clean $K_n$ and every algorithm here applies directly.
- Every assignment is closer to a solution. Formulation 1's assignments mostly have the wrong number of queens entirely; in formulation 2 every assignment is a legal-looking board and $40{,}320$ of the $16{,}777{,}216$ are row-distinct permutations. A space dense in near-solutions is what makes filtering effective.
7. Sudoku as a CSP
Sudoku is the canonical CSP because it is the one you have already solved by hand, using — whether you knew it or not — exactly the algorithms in this chapter.
Variables: each open (blank) square. The given digits are not variables; they are fixed. A typical puzzle has around 50–60 open squares out of 81.
Domains: $\{1,2,\dots,9\}$, so $d = 9$.
Constraints: a 9-way alldiff for each row, each column, and each $3\times 3$ region — 27 constraints in total. ("Region" is the course's word for what puzzle books call a box or block.)
alldiff$(V_1,\dots,V_9)$ holds exactly when all nine values are pairwise distinct. Each mentions nine variables, so this is a higher-order CSP and the constraint-graph machinery of §5 does not literally apply.
Decomposing into binary constraints
The course's alternative: replace each 9-way alldiff by all $\binom{9}{2} = 36$ pairwise $\neq$ constraints. The CSP is then binary and every algorithm below runs unmodified.
How many? Naively $27 \times 36 = 972$, but that double-counts — two squares in the same row and region generate the same constraint twice. Count by squares: each has $8$ row-peers, $8$ column-peers and $8$ region-peers of which $4$ are already row- or column-peers, so $8+8+4 = 20$ distinct peers, and
$$\frac{81 \times 20}{2} = 810$$distinct constraints — divide by two because each is shared by the two squares it relates. So $810$ edges over $81$ nodes.
Higher-order form (27 alldiffs): fewer, larger constraints. Each one can be propagated by a specialised, strong algorithm that sees all nine variables at once.
Binary form (810 inequalities): more, smaller constraints. Generic algorithms (AC-3, forward checking) apply immediately, but propagation is weaker: nine pairwise views of a constraint can miss what the 9-way view catches.
Every higher-order constraint over finite domains can be converted to binary constraints (add auxiliary variables if necessary), so generality is never lost. What is lost is inference strength.
Suppose that after some filtering, three squares in one row have domains
$$D_A = \{1,2\}, \qquad D_B = \{1,2\}, \qquad D_C = \{1,2\}.$$Under the binary reading, every constraint is satisfiable pairwise: $A=1, B=2$ works; $A=1, C=2$ works; $B=1, C=2$ works. Every arc is consistent (see §13), so filtering reports "no problem" and search continues.
Under the 9-way alldiff reading, three variables need three distinct values but only two values exist between them. By the pigeonhole principle the puzzle is already dead. A dedicated alldiff propagator (Régin's algorithm, via bipartite matching) detects exactly this and fails immediately. beyond syllabus The general phenomenon — that local pairwise consistency misses global infeasibility — is the subject of §15.
One observation for §17: when a human solves Sudoku, "this square can only be a 7, so write 7" is precisely minimum remaining values, and "cross 7 off every peer of this square" is precisely forward checking. You already run these algorithms; this chapter names them.
8. Varieties of CSPs
Everything so far assumed discrete variables with finite domains. With $n$ variables and domain size $d$ there are $d^{\,n}$ complete assignments, so brute-force enumeration is $O(d^{\,n})$ — and it stays exponential no matter how clever we are, because:
A Boolean CSP is a finite-domain CSP with $d = 2$. The most famous one is Boolean satisfiability (SAT): variables are propositional atoms with domain $\{\text{true},\text{false}\}$, and each constraint is a clause such as $(x_1 \vee \neg x_2 \vee x_3)$, which forbids exactly one of the eight assignments to those three variables.
SAT is NP-complete (Cook–Levin, 1971). Since SAT is a special case of finite-domain CSP, deciding whether a CSP has a solution is NP-hard in general.
So no algorithm below changes the worst-case complexity. Forward checking, AC-3, MRV and LCV are heuristics and pruning rules: they shrink the tree enormously on realistic instances and change nothing at all in the worst case.
The formalism is wider than finite domains, and the slides hand the rest to 07-380. Domains can be discrete but infinite — job scheduling with integer start times — where an explicit table of allowed tuples is not a data structure and you need a constraint language instead, writing things like $\text{Start}_{J_1} + 5 \le \text{Start}_{J_3}$; linear integer constraints are solvable, nonlinear ones are undecidable (Matiyasevich 1970, Hilbert's tenth problem). Or variables can be continuous — Hubble observation windows — where with linear constraints feasibility is solvable in polynomial time by linear programming.
The landscape: continuous + linear is polynomial, discrete + finite is NP-hard, discrete + infinite + nonlinear is undecidable. Discreteness, not size, is what makes CSPs hard. Only the finite-domain case is examinable here.
9. Varieties of constraints
Constraints vary along a second axis: arity, how many variables one constraint mentions.
Unary constraints
One variable: "South Australia dislikes green", $\text{SA} \neq \text{green}$. These are free, because a statement about which values one variable may take is a domain. Delete the forbidden values once, before search, and forget the constraint existed — the course's "equivalent to reducing domains". So $D_{\text{SA}} = \{\text{red},\text{green},\text{blue}\}$ becomes $\{\text{red},\text{blue}\}$, in $O(d)$, once, forever.
Binary constraints
Exactly two variables: $\text{SA} \neq \text{NSW}$. The focus of the lecture, because these are exactly the constraints that give a constraint graph, and a graph is a thing algorithms can walk.
Higher-order constraints
Three or more: the Sudoku alldiff of §7, or a cryptarithmetic column (two digits, a carry in and a carry out — four variables). A $k$-ary constraint is a hyperedge on $k$ nodes, giving a hypergraph; the standard move is to decompose into binary constraints.
Soft constraints (preferences)
Everything so far is hard: an assignment satisfies the constraints or it is not a solution. Real problems also have preferences — the course's example is "red is better than green".
A soft constraint or preference is often represented by attaching a cost to each variable assignment. The cost of a complete assignment is the sum of the costs of its parts, and we seek the cheapest assignment that satisfies all the hard constraints:
$$\text{minimise} \quad \sum_{i=1}^{n} \text{cost}_i(X_i = v_i) \quad\text{subject to all hard constraints being satisfied.}$$This turns a satisfaction problem into a constrained optimisation problem.
Thinking preferences are a small extension. They are not: they break the defining property of identification problems from §1. Once assignments have costs, the first solution you find is no longer good enough — you must keep searching to prove no cheaper one exists, or use branch-and-bound. Every algorithm below stops at the first solution, and that is only correct because all solutions are equally good.
Concretely: suppose every region costs $0$ for red, $1$ for green and $3$ for blue. The solution from §4 — WA red, NT green, SA blue, Q red, NSW green, V red, T green — costs $0+1+3+0+1+0+1 = 6$. Recolouring T from green to red is still legal (T has no neighbours) and costs $5$, so that solution was valid but not optimal. Under hard constraints only, both are equally correct answers; under preferences, only one is.
10. A CSP is a search problem
Before inventing anything new, check that the search machinery of Chapter 2 already applies. It does, verbatim.
- States are the values assigned so far, i.e. partial assignments such as $\{\text{WA}=\text{red},\ \text{NT}=\text{green}\}$.
- Initial state: the empty assignment, $\{\}$.
- Successor function: assign a value to an unassigned variable — and, in the naive version, it may be any unassigned variable, with any value from its domain.
- Goal test: the current assignment is complete (every variable has a value) and satisfies all constraints.
Three consequences follow immediately:
- Solutions appear only at depth $n$: each step assigns one variable, and the goal test demands all $n$.
- There are no infinite paths and no cycles — every step strictly increases the number of assigned variables.
- The branching factor is $(n-\ell)\,d$ at depth $\ell$: any of the $n - \ell$ unassigned variables, times any of its $d$ values.
That third point is a disaster. Multiplying the branching factors down the tree,
$$\#\text{leaves} \;=\; (n d)\big((n-1)d\big)\cdots(1\cdot d) \;=\; n!\, d^{\,n},$$and for Australia ($n = 7$, $d = 3$),
$$7! \cdot 3^7 = 5040 \times 2187 = 11{,}022{,}480 \ \text{leaves},$$while there are only $3^7 = 2187$ distinct complete assignments. Every assignment is reached $5040$ times, once per ordering of the seven variables. Fixing that is Idea 1 of §11.
The naive algorithm: DFS with the goal test at the bottom
The course's starting point is deliberately dumb: assign a value at each node, and check the constraints only when the assignment is complete — the goal test as the black box of general search. So we build $\text{WA}=\text{red},\ \text{NT}=\text{red}$, already illegal, and extend it through five more variables, generating $3^5 = 243$ complete assignments under a prefix that was dead on arrival and rejecting all $243$ one at a time.
Poll 1 — should we use BFS or DFS?
1. BFS's advantage is unavailable. BFS is preferred when the shallowest goal is the cheapest. Every solution here sits at depth $n$, so BFS's optimality guarantee buys nothing.
2. DFS's disadvantage is unavailable too. DFS is incomplete only because it can dive down an infinite path; this tree has depth exactly $n$, so DFS is complete here for free.
3. Memory — the decisive argument. BFS holds a whole level of the frontier, on the order of $d^{\,n}$ nodes (for Australia at least $2187$ partial assignments). DFS stores one root-to-leaf path: $O(n)$ nodes, or $O(nd)$ if it also remembers which values remain at each level. 7 nodes versus thousands, and the gap is exponential in $n$.
4. Time to first solution. BFS reaches its first complete assignment only after enumerating every incomplete one; DFS gets there in $n$ expansions. Any solution is as good as any other, so reaching the bottom fast is exactly what we want.
So: depth-first search, with improvements bolted on.
11. Backtracking search
Backtracking search is the basic uninformed algorithm for solving CSPs, and $$\textbf{backtracking search} \;=\; \textbf{DFS} \;+\; \textbf{two improvements}.$$
Idea 1 — one variable at a time
Variable assignments are commutative:
$$[\,\text{WA}=\text{red} \text{ then } \text{NT}=\text{green}\,] \quad\text{is the same state as}\quad [\,\text{NT}=\text{green} \text{ then } \text{WA}=\text{red}\,].$$Both are the set $\{\text{WA}=\text{red},\ \text{NT}=\text{green}\}$, and a set has no order, so a search exploring both orderings explores each state $\ell!$ times at depth $\ell$. The fix is one line of code: at each node, fix one variable and branch only on its values. Which variable is up to you; what matters is that you pick one and do not consider the others at this node.
At the root we now generate $3$ children instead of $21$. Could we lose a solution? No. Let $S$ be any solution, a complete assignment, and suppose we branch on $\text{WA}$. $S$ gives $\text{WA}$ some value $v$, and the child $\text{WA}=v$ exists because we generate a child for every value in $D_{\text{WA}}$ — so $S$ lies in its subtree. Recurse: at every node, whichever variable we branch on, $S$ assigns it some value and that branch exists. By induction $S$ is reachable.
What we discarded were alternative orderings of the same assignments, never alternative assignments, so completeness is preserved exactly. The proof also shows that any variable-selection rule is sound — which is what licenses MRV in §17. $\square$
Quantifying the saving. Branching factor drops from $nd$ to $d$; the leaf count drops from $n!\,d^{\,n}$ to $d^{\,n}$; that is a factor of $n!$.
| Instance | Naive leaves $n!d^n$ | One-variable-at-a-time $d^n$ | Saving $n!$ |
|---|---|---|---|
| Australia, $n=7, d=3$ | $11{,}022{,}480$ | $2{,}187$ | $5{,}040\times$ |
| 8-queens, $n=8, d=8$ | $\approx 6.8\times 10^{11}$ | $16{,}777{,}216$ | $40{,}320\times$ |
| Sudoku, $n=55, d=9$ | $\approx 10^{126}$ | $\approx 10^{52}$ | $\approx 10^{73}\times$ |
Idea 2 — check constraints as you go
Consider only values that do not conflict with previous assignments. Testing each candidate against the already-assigned neighbours costs a little computation, but nothing compared with the subtree avoided. The course calls this the incremental goal test: a small test at every node instead of one big test at depth $n$. Concretely, with $\text{WA}=\text{red}$ assigned, we simply never generate the child $\text{NT}=\text{red}$ — which naive DFS would have grown $3^5 = 243$ complete assignments beneath before noticing.
The algorithm in full
function BACKTRACKING-SEARCH(csp) returns a solution, or failure
return RECURSIVE-BACKTRACKING({}, csp)
function RECURSIVE-BACKTRACKING(assignment, csp) returns a solution, or failure
if assignment is complete then
return assignment # every variable has a value, and
# every value was checked on the way in
var <- SELECT-UNASSIGNED-VARIABLE(VARIABLES[csp], assignment, csp) # decision point 1
for each value in ORDER-DOMAIN-VALUES(var, assignment, csp) do # decision point 2
if value is consistent with assignment given CONSTRAINTS[csp] then # decision point 3
add {var = value} to assignment
result <- RECURSIVE-BACKTRACKING(assignment, csp)
if result != failure then
return result
remove {var = value} from assignment # <- this line is the "backtrack"
return failure # no value for var works; tell the caller
Three lines are worth reading twice.
if assignment is complete then return assignment— no constraint check here, and none is needed: every value was checked when it was added, so a complete assignment is automatically consistent. The course states this explicitly ("no need to check constraints for a complete assignment"); it is the pay-off of the incremental goal test.if value is consistent with assignment— Idea 2. It checks only constraints betweenvarand the already assigned variables; constraints between two unassigned variables are not testable yet.remove {var = value} from assignment— the actual backtrack. An implementation that maintains filtered domains must also restore the domains here.return failureis reached when the loop is exhausted: no value ofvarworks, so the subtree is dead. (SELECT-UNASSIGNED-VARIABLEandORDER-DOMAIN-VALUESare sound for any choice and any order; MRV and LCV make them smart in §17.)
Backtracking = DFS + variable ordering + fail-on-violation. And the decision points — the three places where a smarter policy can be dropped in without touching correctness — are: (1) which variable next (§17, MRV), (2) which value first (§17, LCV), and (3) how hard to look for trouble at each node (§12 forward checking, §14 AC-3). A fourth, beyond this course, is (4) how far to jump back on failure — plain backtracking undoes one assignment; backjumping undoes several at once. beyond syllabus
With just these two improvements, backtracking search solves n-queens for $n \approx 25$. That is the honest number from the course: not thousands, twenty-five. Hold onto it — by §17 the same basic algorithm will handle $n = 1000$.
12. Filtering, part 1: forward checking
Backtracking still wastes effort in a specific, diagnosable way. The state $\{\text{WA}=\text{red},\ \text{Q}=\text{green},\ \text{V}=\text{blue}\}$ is perfectly consistent — no two of WA, Q, V even share a border — but it is already dead: SA borders all three, so SA can be neither red nor green nor blue. Plain backtracking will not discover this until it actually gets to SA, after building every combination of NT and NSW above it.
Filtering: keep track of the domains of the unassigned variables, and cross off bad options as you go. Failure is detected the moment some variable has no values remaining — which can be long before that variable is reached by the search.
The simplest filtering rule. After a variable is assigned a value, check the constraints it participates in and cross off the values of the unassigned variables that violate them. For map colouring: after $X = c$, delete $c$ from the domain of every unassigned neighbour of $X$. If any domain becomes empty, fail and backtrack immediately.
Forward checking is the pencil marks in a Sudoku grid. You do not just write a 7 into a square; you immediately rub the little 7 out of every square in that row, column and box. The instant a square has no pencil marks left, you know you went wrong earlier — without having to reach that square in your solving order.
The Australia trace
Assign $\text{WA}=\text{red}$, then $\text{Q}=\text{green}$, then $\text{V}=\text{blue}$, maintaining domains. Every domain starts $\{\text{R},\text{G},\text{B}\}$. Borders: WA–NT, WA–SA, NT–SA, NT–Q, SA–Q, SA–NSW, SA–V, Q–NSW, NSW–V.
- WA = red. Unassigned neighbours NT and SA: delete R from both. Q, NSW, V and T do not border WA and are untouched.
- Q = green. Unassigned neighbours NT, SA, NSW: delete G from each. NT $\{$G,B$\} \to \{$B$\}$, SA $\{$G,B$\} \to \{$B$\}$, NSW $\{$R,G,B$\} \to \{$R,B$\}$. No domain is empty, so forward checking permits the search to continue.
- V = blue. Unassigned neighbours SA and NSW: delete B from each. NSW $\{$R,B$\} \to \{$R$\}$, fine. SA $\{$B$\} \to \{\,\}$ — empty. FAIL: a variable with no possible values. Backtrack.
| Variable | Initial | after WA = red | after Q = green | after V = blue |
|---|---|---|---|---|
| WA | R G B | R | R | R |
| NT | R G B | |||
| Q | R G B | R G B | G | G |
| NSW | R G B | R G B | R | R |
| V | R G B | R G B | R G B | B |
| SA | R G B | |||
| T | R G B | R G B | R G B | R G B |
Forgetting to restore the crossed-off values when you backtrack. The deletions made after assigning $\text{V}=\text{blue}$ are valid only while that assignment stands. When the search undoes $\text{V}=\text{blue}$ and tries $\text{V}=\text{green}$, SA must get its B back. In code, either save a copy of the domains at each node, or keep a trail of (variable, value) deletions to undo. A filtering bug of this kind does not crash — it silently reports "no solution" for solvable problems, which is much worse.
Same Australia CSP, same three colours, but a different assignment order. Apply forward checking after each of these assignments, in order, and report the first failure:
NT = red, NSW = green, V = blue
Borders again: WA–NT, WA–SA, NT–SA, NT–Q, SA–Q, SA–NSW, SA–V, Q–NSW, NSW–V.
Step 1 — Which domains does NT = red touch?
NT's neighbours are WA, SA and Q. Delete red from each of them.
WA: $\{$G, B$\}$ · SA: $\{$G, B$\}$ · Q: $\{$G, B$\}$. Everything else still $\{$R, G, B$\}$. No empty domain, continue.
Step 2 — Now NSW = green
NSW's neighbours are SA, Q and V. Delete green from each.
SA: $\{$G, B$\} \to \{$B$\}$ · Q: $\{$G, B$\} \to \{$B$\}$ · V: $\{$R, G, B$\} \to \{$R, B$\}$.
Still no empty domain. But pause and look: SA and Q are both down to $\{$B$\}$, and SA–Q is a border. The problem is already dead — and forward checking has no idea, for exactly the reason flagged in Figure 4.
Step 3 — Now V = blue
Check it is allowed first: V's assigned neighbours are NSW (green). Blue is in $D_V = \{$R, B$\}$, so the assignment is consistent and we make it.
V's unassigned neighbour is SA. Delete blue from SA: $\{$B$\} \to \{\,\}$.
Empty domain at SA. FAIL. Backtrack on V.
Answer & the takeaway
Final domain table before the failure is announced:
| WA | NT | Q | NSW | V | SA | T | |
|---|---|---|---|---|---|---|---|
| domain | G B | R | B | G | B | ∅ | R G B |
The first failure is at SA, after the third assignment.
Takeaway: the failure was detectable after the second assignment (SA and Q both forced to blue across a border), but forward checking only ever propagates assigned → unassigned. It took a third assignment to make the contradiction visible to it. One wasted level of search does not sound like much; multiply by $d$ at every node above and it is a large constant factor. That is the gap arc consistency closes.
13. Consistency of a single arc
Forward checking is a special case of something more general, and naming the general thing lets us strengthen it. The general thing is arc consistency.
Let $X$ and $Y$ be two variables joined by a constraint, and consider the directed arc $X \to Y$. The arc has a tail ($X$) and a head ($Y$).
The arc $X \to Y$ is consistent iff for every value $x$ in the tail's domain $D_X$, there is some value $y$ in the head's domain $D_Y$ such that the pair $(x,y)$ does not violate the constraint between $X$ and $Y$.
$x$ ranges over the values left in $D_X$, $y$ over those left in $D_Y$, and "does not violate" means the pair is allowed by the constraint (for map colouring, $x \neq y$). A $y$ that works for a given $x$ is a support for $x$. So: the arc $X \to Y$ is consistent iff every value in the tail has at least one support in the head.
To enforce arc consistency on $X \to Y$: remove from $D_X$ every value that has no support in $D_Y$.
The head's domain is not touched. Not shrunk, not grown, not read-and-modified — only read.
The lecture prints "Remember: delete from the tail!" at the bottom of fifteen consecutive slides. That is not decoration; it is the single most common place to go wrong by hand. Arc $X \to Y$ ⟹ values die in $X$.
Forward checking, re-read as arc consistency
The course writes forward checking as
$$\text{enforce } X \to Y \quad\text{for all } (X,Y) \in E \text{ with } Y \text{ newly assigned.}$$Unpack it. $Y$ was just assigned, so $D_Y$ is a singleton $\{c\}$, and $X$ ranges over $Y$'s neighbours. Enforcing $X \to Y$ deletes from $D_X$ every value with no support in $\{c\}$ — and for a $\neq$ constraint the only value without support is $c$ itself. So those arcs delete exactly $c$ from each neighbour's domain: forward checking, verbatim.
Forward checking = enforce arc consistency on the arcs pointing at the newly assigned variable, once.
Full arc consistency = enforce it on every arc, repeatedly, until nothing changes.
Full arc consistency is strictly stronger: it makes every deletion forward checking makes, and possibly more, for two separate reasons worth keeping apart.
- More arcs. Forward checking only examines arcs $X \to Y$ with $Y$ just assigned, never an arc between two unassigned variables. Arc consistency examines all of them — which is exactly how it catches "NT and SA are both forced to blue and share a border".
- Propagation. When forward checking removes a value from $D_X$ it stops. Arc consistency asks whether shrinking $D_X$ destroyed the support of some value in a third variable $W$ neighbouring $X$; if so that value dies too, which may destroy support elsewhere, and so on. Deletions cascade. Forward checking has no cascade — it is one layer deep.
Let $D^{\text{FC}}$ be the domains after forward checking on the assignment of $Y$, and $D^{\text{AC}}$ the domains after enforcing full arc consistency from the same starting point. Claim: $D^{\text{AC}}_X \subseteq D^{\text{FC}}_X$ for every $X$.
The arcs forward checking enforces are a subset of the arcs arc consistency enforces, and enforcing an arc only ever removes values. A value removed by enforcing some arc is removed by any procedure that enforces that arc and reaches a fixed point. Hence every value FC removes, AC also removes; AC may additionally remove values via arcs FC never looked at. $\square$
"Strictly": the Australia trace of §14 exhibits a case where AC empties a domain and FC does not. Corollary: arc consistency detects failure at least as early as forward checking, and never later.
14. AC-3: enforcing arc consistency on the whole CSP
Knowing how to enforce one arc, we need every arc consistent at once. The difficulty is that the arcs interact: enforcing $X \to Y$ shrinks $D_X$, which can break an arc $W \to X$ that was consistent a moment ago. One pass is not enough.
The simplistic algorithm
repeat
changed <- false
for each ordered pair (X, Y) of variables joined by a constraint do
if enforcing X -> Y removes any value then changed <- true
until changed = false
Correct — it stops only at a fixed point, and a fixed point is by definition arc-consistent — and wasteful: one full cycle costs $O(|E| d^2)$ even if the only thing that changed was a single value, and you may need many cycles.
AC-3
AC-3 (Arc Consistency algorithm #3, Mackworth 1977) is the same idea with a queue of arcs still to check, so that it ignores constraints that have not been modified since they were last analysed. An arc goes back on the queue only when something it depends on has actually changed.
function AC-3(csp, queue) returns false if an inconsistency is found, true otherwise
# queue defaults to ALL directed arcs (Xi, Xj) of the constraint graph --
# both directions of every edge
while queue is not empty do
(Xi, Xj) <- REMOVE-FIRST(queue)
if REVISE(csp, Xi, Xj) then # Di actually shrank
if size of Di = 0 then
return false # wipeout: this CSP branch is dead
for each Xk in NEIGHBORS(Xi) \ {Xj} do
add (Xk, Xi) to queue # Di shrank, so everyone who
# depends on Di must be re-checked
return true
function REVISE(csp, Xi, Xj) returns true iff we removed something from Di
revised <- false
for each x in Di do
if no value y in Dj satisfies the constraint between Xi and Xj given x then
delete x from Di # DELETE FROM THE TAIL
revised <- true
return revised
When a value is deleted from $D_X$, re-enqueue the arc $W \to X$ for every neighbour $W$ of $X$.
Why into $X$ and not out of it? Because $D_X$ is the head of the arcs $W \to X$, and a head's domain is what supplies supports, so shrinking $D_X$ can destroy the support of some value in $D_W$. Arcs out of $X$ are unaffected: a smaller tail cannot create new unsupported values in some other variable.
The lecture enqueues all neighbours $W$, including the head $X_j$ just compared against; the textbook writes $\text{NEIGHBORS}(X_i) \setminus \{X_j\}$. Both are correct — see below — and skipping is a small optimisation. The trace follows the lecture.
Suppose REVISE$(X_i, X_j)$ deleted the value $x$ from $D_i$. Could that deletion cost some $y \in D_j$ its support, making $X_j \to X_i$ newly inconsistent? Only if $y$'s only support in $D_i$ had been $x$, i.e. $(y,x)$ satisfies the constraint. But $x$ was deleted precisely because no value of $D_j$ supports it: for every $y' \in D_j$, $(x,y')$ violates the constraint. Taking $y' = y$ gives that $(x,y)$ violates it, contradicting $(y,x)$ satisfying it — for binary constraints $(x,y)$ is allowed iff $(y,x)$ is, each read in the right order.
So no value of $D_j$ ever depended on a value we deleted from $D_i$, and $X_j \to X_i$ cannot have been broken. $\square$
The lecture's Australia trace, compressed
We are inside backtracking search, running AC-3 after each assignment, with WA = red and then Q = green.
Phase 1 — AC-3 after WA = red. $D_{\text{WA}} = \{$R$\}$, everything else $\{$R,G,B$\}$. The queue is seeded with the arcs pointing at the newly assigned variable, $\text{SA}\to\text{WA}$ and $\text{NT}\to\text{WA}$. Each deletes R from its tail — $D_{\text{SA}} = D_{\text{NT}} = \{$G,B$\}$ — and each enqueues every arc into the variable that shrank (WA→SA, NT→SA, Q→SA, NSW→SA, V→SA; then WA→NT, SA→NT, Q→NT). All of those pass: WA→SA finds R supported by G $\in D_{\text{SA}}$, NT→SA compares $\{$G,B$\}$ with $\{$G,B$\}$ and finds G supported by B and B by G, and so on. Nothing more is deleted, so nothing more is enqueued and the queue drains. The CSP is arc-consistent and search continues, leaving $D_{\text{WA}} = \{$R$\}$, $D_{\text{NT}} = D_{\text{SA}} = \{$G,B$\}$ and $D_{\text{Q}} = D_{\text{NSW}} = D_{\text{V}} = D_{\text{T}} = \{$R,G,B$\}$.
Phase 2 — AC-3 after Q = green. Set $D_{\text{Q}} = \{$G$\}$ and seed the queue with the arcs into Q: $\text{NT}\to\text{Q}$, $\text{SA}\to\text{Q}$, $\text{NSW}\to\text{Q}$.
| Arc popped | REVISE result | Enqueued |
|---|---|---|
| NT → Q | G has no support in $\{$G$\}$ → delete G from NT. $D_{\text{NT}} = \{$B$\}$. | every arc into NT: WA→NT, SA→NT, Q→NT |
| SA → Q | G unsupported → delete G from SA. $D_{\text{SA}} = \{$B$\}$. | every arc into SA |
| NSW → Q | G unsupported → delete G from NSW. $D_{\text{NSW}} = \{$R,B$\}$. | every arc into NSW |
| WA → NT | $\{$R$\}$ against $\{$B$\}$: R is supported by B. No change. | — |
| SA → NT | $D_{\text{SA}} = \{$B$\}$, $D_{\text{NT}} = \{$B$\}$. Is B supported? The only $y \in D_{\text{NT}}$ is B, and B = B violates SA ≠ NT. Delete B from SA. $D_{\text{SA}} = \varnothing$ !!! | AC-3 returns false — wipeout. |
Note the mechanism, because it is the whole point: the deletion at NT in the first step put SA→NT onto the queue, and processing that re-queued arc is what found the contradiction. That cascade is what forward checking does not have.
Backtrack on the assignment of Q. AC-3 has proved that no completion of $\{\text{WA}=\text{red},\ \text{Q}=\text{green}\}$ exists, so the search must undo $\text{Q}=\text{green}$ and try $\text{Q}=\text{blue}$ instead — without ever assigning NT, SA, NSW, V or T.
Compare with §12: forward checking, given the same two assignments, would have produced $D_{\text{NT}} = \{$B$\}$, $D_{\text{SA}} = \{$B$\}$, $D_{\text{NSW}} = \{$R,B$\}$ — no empty domain — and continued happily. It needed a third assignment before it noticed. Arc consistency detects failure earlier than forward checking.
Where AC-3 fits, and what it costs
AC-3 runs either as a preprocessor — once on the raw CSP before any search, cheaply shrinking every domain permanently, and occasionally proving the whole problem infeasible before you begin — or after each assignment inside the recursion, seeded with the arcs into the just-assigned variable, with its effects undone on backtrack.
Cost. One call to AC-3 is $O(n^2 d^3)$ (derived in §16), and backtracking search calls it at every node. Forward checking, by contrast, costs $O(\text{deg}(X) \cdot d) $ — a handful of deletions. You are buying pruned subtrees with CPU time, and the exchange rate is not always favourable.
Three specific ways the trade can go badly:
- Loosely constrained problems. If domains are large relative to the constraints, AC-3 sweeps every arc and deletes nothing. You paid $O(n^2d^3)$ for zero information, at every node.
- Deep search trees. The cost is per node and the tree can have exponentially many nodes, so a constant-factor slowdown per node is a constant-factor slowdown overall — which you only recoup if the pruning actually removes subtrees.
- Redundant work. Much of what AC-3 recomputes at a node was already computed at the parent. Incremental variants (AC-4, AC-6, AC-2001) keep support counts between calls to avoid this, at the price of memory and bookkeeping. beyond syllabus
There is also a second, subtler downside, and it is the whole of the next section: arc consistency can succeed and still be wrong. An arc-consistent CSP may have no solution at all, so passing the AC-3 test proves nothing.
Variables $A, B, C$ with domains $D_A = D_B = D_C = \{1,2,3\}$ and two constraints:
$A \lt B$ and $B \lt C$.
The constraint graph is the path $A - B - C$. Run AC-3 with the queue seeded with all four directed arcs, in the order $A{\to}B$, $B{\to}A$, $B{\to}C$, $C{\to}B$. Report the final domains.
Step 1 — Arc $A \to B$
Tail $A$, head $B = \{1,2,3\}$. For each $a \in D_A$ we need some $b \in D_B$ with $a \lt b$.
- $a=1$: $b=2$ works ✓
- $a=2$: $b=3$ works ✓
- $a=3$: need $b \gt 3$, none exists ✗ → delete 3
$D_A = \{1,2\}$. Something changed, so enqueue the arcs into $A$: only $B \to A$ (which is already queued; a duplicate is harmless).
Queue: $B{\to}A$, $B{\to}C$, $C{\to}B$, $(B{\to}A)$.
Step 2 — Arc $B \to A$
Careful with direction: the constraint is $A \lt B$, so from $B$'s side each $b$ needs some $a \in D_A = \{1,2\}$ with $a \lt b$.
- $b=1$: need $a \lt 1$, none ✗ → delete 1
- $b=2$: $a=1$ ✓
- $b=3$: $a=1$ ✓
$D_B = \{2,3\}$. Enqueue arcs into $B$: $A \to B$ and $C \to B$.
Step 3 — Arc $B \to C$
Constraint $B \lt C$; tail $B = \{2,3\}$, head $C = \{1,2,3\}$.
- $b=2$: $c=3$ ✓
- $b=3$: need $c \gt 3$, none ✗ → delete 3
$D_B = \{2\}$. Enqueue arcs into $B$ again: $A \to B$, $C \to B$.
Step 4 — Arc $C \to B$, then the re-queued arcs
$C \to B$: each $c$ needs some $b \in D_B = \{2\}$ with $b \lt c$.
- $c=1$: $2 \lt 1$? no ✗ → delete 1
- $c=2$: $2 \lt 2$? no ✗ → delete 2
- $c=3$: $2 \lt 3$ ✓
$D_C = \{3\}$. Enqueue arcs into $C$: $B \to C$.
Now $A \to B$ with $D_A = \{1,2\}$, $D_B = \{2\}$: $a=1$ needs $1 \lt 2$ ✓; $a=2$ needs $2 \lt 2$ ✗ → delete 2. $D_A = \{1\}$. Enqueue $B \to A$.
Remaining arcs ($B \to C$, $B \to A$, and any duplicates) now find everything supported: $D_A=\{1\}, D_B=\{2\}, D_C=\{3\}$ with $1 \lt 2 \lt 3$. Nothing changes, queue drains.
Answer & the takeaway
Final domains: $D_A = \{1\}$, $D_B = \{2\}$, $D_C = \{3\}$. Every domain is a singleton, so arc consistency alone has solved the CSP — zero backtracking was required.
Takeaway: that was not luck. This constraint graph is a tree (a path is a tree), and for tree-structured CSPs, enforcing arc consistency in the right order guarantees a backtrack-free solve. That is the structure result flagged in §18. Notice also how the deletion at $B$ in step 3 propagated back to $A$ in step 4 — the cascade forward checking does not have.
15. The limitations of arc consistency
AC-3 returning true feels like a certificate. It is not one. After enforcing arc consistency you may be in any of three situations:
One solution left. Every domain is a singleton and they are mutually consistent — as in Guided problem 2. Read the answer off the domains; you are done.
Many solutions left. Domains have shrunk but several still hold multiple values. You have a smaller CSP, not an answer. Keep searching.
No solutions left — and you do not know it. Every arc is consistent, every domain is non-empty, and yet no complete assignment satisfies all constraints. AC-3 reports success on an unsolvable problem.
Case 3 is the one to internalise, and it is what the lecture's "What went wrong here?" slide is pointing at.
Three variables $X$, $Y$, $Z$. Domains $D_X = D_Y = D_Z = \{\text{red}, \text{green}\}$. Constraints: $X \neq Y$, $Y \neq Z$, $X \neq Z$ — a triangle.
Every arc is consistent. Check one and the rest follow by symmetry. Arc $X \to Y$: does red in $D_X$ have a support? Yes — green in $D_Y$, and red ≠ green. Does green in $D_X$ have a support? Yes — red in $D_Y$. So nothing is deleted. The same holds for all six directed arcs. AC-3 drains its queue without a single deletion and returns true.
There is no solution. The three variables must take pairwise distinct values, so we need three distinct values; we have two. By the pigeonhole principle, two of $X, Y, Z$ share a value, and those two are constrained to differ. Contradiction. (Equivalently: a triangle is a 3-clique and needs 3 colours; we offered 2.)
What went wrong: nothing went wrong locally. Every pair is individually satisfiable. The contradiction lives in a three-variable interaction, and arc consistency — by construction — only ever looks at two variables at a time.
- Arc consistency only checks local consistency conditions — two variables and one constraint at a time.
- An arc-consistent CSP can still be unsolvable, and AC-3 will not say so. Non-empty domains are necessary, not sufficient.
- Arc consistency still runs inside a backtracking search. It is a pruning subroutine, never a solver. The search is what actually decides satisfiability; AC-3 just makes it do less work. This is why §14's pseudocode returns "true/false", not "solution".
16. The cost of one run of AC-3
The lecture derives the bound in four steps, each one line.
Step 1 — an arc is enqueued only after a value is removed. Look at the pseudocode: add (Xk, Xi) to queue sits inside if REVISE(...) then, and REVISE returns true only when it actually deleted something. (The one exception is the initial queue, handled in step 3.) So "how many arcs get processed?" reduces to "how many deletions can happen?".
Step 2 — there are at most $O(nd)$ removals in the whole run. There are $n$ variables, each with a domain of at most $d$ values, so there are at most $nd$ values in existence. Every removal destroys one value permanently — AC-3 never puts a value back — so there can be at most $nd$ removals ever. This is the crucial monotonicity argument: domains only shrink, which is what makes the algorithm terminate at all.
Step 3 — each removal enqueues at most $n$ arcs, so $O(n^2 d)$ arc insertions. When a value dies in $D_{X_i}$, we enqueue one arc per neighbour of $X_i$. A variable has fewer than $n$ neighbours (at most $n-1$), so each removal adds $\le n$ arcs. Multiply by the $O(nd)$ removals: $O(n^2 d)$ insertions. The initial queue contributes $2|E| \le n(n-1) = O(n^2)$ arcs, which is dominated by $O(n^2d)$ whenever $d \ge 1$. Each insertion is processed at most once, so the while loop runs $O(n^2d)$ times.
Step 4 — processing one arc costs $O(d^2)$. REVISE$(X_i, X_j)$ loops over the $\le d$ values $x \in D_i$ and, for each, scans the $\le d$ values $y \in D_j$ looking for a support. That is $d \times d$ constraint checks in the worst case, each $O(1)$.
Multiply. $$\underbrace{O(n^2 d)}_{\text{arcs processed}} \times \underbrace{O(d^2)}_{\text{cost per arc}} \;=\; \boxed{O(n^2 d^3)}$$ for a single run of AC-3.
Sanity-check the factors against Australia: $n = 7$, $d = 3$, giving $n^2 d^3 = 49 \times 27 = 1323$ constraint checks as an upper bound for one call. The trace in §14 used about 15 arcs and a few dozen checks — the bound is loose, as worst-case bounds are, but it is the right shape.
Textbooks often state AC-3 as $O(e\, d^3)$ where $e = |E|$ is the number of edges (constraints) rather than $O(n^2d^3)$. The two agree: a binary constraint graph on $n$ nodes has $e \le \binom{n}{2} = O(n^2)$ edges, and the derivation above used "$\le n$ neighbours" exactly where a finer count would use the degree. For sparse graphs — a road map, a schedule — $e$ is closer to $O(n)$ and AC-3 is much faster than the $n^2$ form suggests.
The tightened bound. beyond syllabus Zhang and Yap (2001) showed that the true worst case of AC-3 is $O(n^2 d^2)$, a factor of $d$ better, by not repeating the $d^2$ support scan from scratch on every visit to an arc. The lecture marks this "not required"; the classroom answer is $O(n^2d^3)$.
This is the cost of one run. The whole backtracking algorithm with AC-3 calls AC-3 many times — once per node of the search tree, and the tree can have $O(d^n)$ nodes. The total is therefore $O(d^n \cdot n^2 d^3)$ in the worst case, which is worse than plain backtracking's worst case. AC-3 is a bet that the pruning saves more nodes than the filtering costs. On structured problems it wins enormously; on loose ones it loses. See the downside box in §14.
Where do you run AC-3? — the lecture's question, answered
1. Once as a preprocessor, before search starts, with the queue seeded with all directed arcs. This is pure profit: one $O(n^2d^3)$ call, and every value it deletes stays deleted at every node, forever. If it returns false, the CSP is unsatisfiable and you never search at all.
2. After each assignment, inside the recursion, seeded only with the arcs $W \to X$ for the neighbours $W$ of the just-assigned $X$. Seeding all arcs would be correct but wasteful: the only domain that just changed is $D_X$, so the only arcs that can newly fail are those whose head is $X$ — everything else follows by propagation.
function BACKTRACKING-WITH-AC3(csp) returns a solution, or failure
if AC-3(csp, queue = ALL-ARCS(csp)) = false then
return failure # preprocessing pass: whole CSP is dead
return RECURSE({}, csp)
function RECURSE(assignment, csp) returns a solution, or failure
if assignment is complete then return assignment
var <- SELECT-UNASSIGNED-VARIABLE(...) # MRV (see section 17)
for each value in ORDER-DOMAIN-VALUES(var, ...) do # LCV (see section 17)
if value is consistent with assignment then
add {var = value} to assignment
saved <- copy of all current domains # so we can undo the filtering
D[var] <- {value}
queue <- { (W, var) : W in NEIGHBORS(var) } # only the arcs into var
if AC-3(csp, queue) then # no domain wiped out
result <- RECURSE(assignment, csp)
if result != failure then return result
restore all domains from saved # undo filtering, then
remove {var = value} from assignment # undo the assignment
return failure
Two details are easy to get wrong and both are fatal:
saved <- copy of all current domainsmust happen beforeD[var] <- {value}, and the restore must happen on every exit from the loop body that does not return a solution — including the path where AC-3 returns false.- The queue is seeded with arcs pointing at
var, i.e. $(W, \textit{var})$ with $W$ the tail. Seeding $(\textit{var}, W)$ instead is the classic direction bug: those arcs are already consistent, the propagation never starts, and your solver silently degenerates into plain backtracking.
17. Ordering: MRV and LCV
Filtering answered "can we detect inevitable failure early?". Ordering answers the lecture's other two questions about improving backtracking: which variable should be assigned next (the SELECT-UNASSIGNED-VARIABLE hook) and in what order should its values be tried (the ORDER-DOMAIN-VALUES hook). Both are pure speed — correctness does not depend on them, since the soundness proof in §11 allowed any choice. And, the part students consistently get backwards, the two questions have opposite answers.
Variable ordering: minimum remaining values
Minimum remaining values (MRV): choose the unassigned variable with the fewest legal values left in its filtered domain. Also called the most constrained variable heuristic, or "fail-fast" ordering.
MRV needs filtering to be running — "values left in its domain" is only meaningful if someone is crossing values off. In practice MRV is always paired with forward checking or AC-3.
Because you must satisfy every variable, so the only question is when you find out you cannot. Ordering never changes whether a solution exists; it changes the depth at which failure becomes visible. Put the variable most likely to fail as close to the root as possible.
Reason 1 — the pruned subtree is exponentially bigger near the top. Discovering a doomed variable at depth $k$ discards about $d^{\,n-k}$ leaves: $d^{\,n-1}$ at depth 1, only $d$ at depth $n-1$. If SA is unsatisfiable under the current filtering, assigning it first fails once; assigning it last re-discovers the same failure at the bottom of every branch above it — up to $3^5 = 243$ separate times, each after five wasted assignments.
Reason 2 — the branching factor at this node is smallest. The node has exactly $|D_X|$ children, so the smallest $|D_X|$ makes the tree narrowest at the top. Compare $2 \times 9$ with $9 \times 2$: the same 18 leaves, reached through 2 internal nodes instead of 9.
Reason 3 — the $|D_X| = 0$ case is free. MRV selects an empty domain (0 is the minimum), the for loop over its values runs zero times, and we return failure immediately. MRV subsumes the wipeout check.
What max remaining values would do: put the easy, flexible variables at the top and the bottleneck at the bottom — the worst possible shape. You would branch widely on variables that were never going to cause trouble, then hit the same contradiction over and over at the leaves.
Tie-breaking. Several variables often tie on domain size — at the start of Australia all seven have three values. The standard tie-breaker is the degree heuristic of §5: among the tied variables, pick the one with the most constraints on unassigned variables. In Australia that is SA (degree 5), precisely the variable we want at the root.
Value ordering: least constraining value
Least constraining value (LCV): given a choice of variable, try its values in order of how few options each rules out for the remaining variables — the least constraining value first.
Note that it may take some computation to determine this: to know how many options a value rules out you effectively have to re-run the filtering for each candidate value and count the deletions.
Because you only need ONE value to work. A variable node in the search tree is a disjunction: the subtree succeeds if any child succeeds. So try the value most likely to work first — and the value that leaves its neighbours the most options is the one under which solutions are most likely to survive.
What happens if you get it backwards. The most constraining value maximises the chance that the first child fails. But that failure ends nothing — you still have to go back and try the other values. You have simply guaranteed that the first attempt is wasted.
Caveat, stated honestly. LCV only helps when you are looking for one solution and a solution exists. If the subproblem has no solution at all, or you are enumerating all solutions, you must try every value regardless and the order is irrelevant. LCV is a bet on success; MRV is a bet on failure. Both bets pay off on realistic instances, which is why they are used together.
These two heuristics point in opposite directions, and that is not an inconsistency. It follows from the logical structure of the search tree.
| Choosing a variable | Choosing a value | |
|---|---|---|
| Logical form | AND — every variable must get a value | OR — some value must work |
| Node type | you will visit all of them eventually | you stop at the first child that succeeds |
| What you want | fail fast | succeed fast |
| So pick the | most constrained (fewest values) | least constraining (kills fewest options) |
| Heuristic | MRV | LCV |
| Payoff when | the branch is doomed — you learn it at the top | the branch is solvable — you land on it first |
One sentence to memorise: you must satisfy every variable, so look for trouble; you only need one value, so look for the easiest one. If you ever find yourself writing "choose the most constraining value", you have mixed up the AND with the OR.
Australia, with $\text{WA} = \text{red}$ and $\text{Q} = \text{green}$ already assigned and forward checking applied, so:
NT {B} SA {B} NSW {R,B} V {R,G,B} T {R,G,B}
Say we are choosing a value for NSW, whose domain is $\{$R, B$\}$. Count the deletions each choice would cause among unassigned neighbours (SA and V):
- NSW = red: removes red from $D_{\text{SA}} = \{$B$\}$ → not present, 0 deletions. Removes red from $D_{\text{V}} = \{$R,G,B$\}$ → 1 deletion. Total 1.
- NSW = blue: removes blue from $D_{\text{SA}} = \{$B$\}$ → 1 deletion, and it empties SA. Removes blue from $D_{\text{V}}$ → 1 deletion. Total 2, with an immediate wipeout.
LCV therefore orders red before blue, and red is indeed the only one that can possibly lead anywhere. Note the cost: producing this ranking required simulating the filtering twice, once per candidate value — that is the "some computation" the slides warn about.
The payoff. The lecture's headline: these ordering ideas make 1000-queens feasible, against the $n \approx 25$ plain backtracking managed in §11. Same algorithm, same exponential worst case — but filtering and ordering together keep realistic instances out of the exponential region entirely.
Australia, three colours, backtracking with forward checking, MRV (degree tie-break) and LCV. Nothing is assigned yet; all seven domains are $\{$R, G, B$\}$.
Work out: (a) which variable is assigned first and why, (b) which value LCV picks for it, (c) which variable comes second, (d) which value LCV picks for that.
Step 1 — (a) The first variable
MRV compares domain sizes: all seven are $3$. A seven-way tie, so MRV alone cannot decide and the degree heuristic breaks it — count constraints on unassigned variables:
| Variable | WA | NT | Q | NSW | V | SA | T |
|---|---|---|---|---|---|---|---|
| degree | 2 | 3 | 3 | 3 | 2 | 5 | 0 |
SA wins with degree 5. Assigning SA first immediately filters five other domains — the largest possible amount of information from one move — and SA is the variable most likely to become impossible later, so we want its failure (if any) at the root.
Step 2 — (b) LCV for SA
All three colours are symmetric right now: every neighbour has the full domain $\{$R,G,B$\}$, so whichever colour we choose deletes exactly one value from each of the five neighbours — 5 deletions either way.
A genuine three-way tie, so LCV is indifferent; take red by convention. This is worth saying out loud rather than inventing a reason: in a symmetric problem the value heuristic has nothing to work with, and pretending otherwise is a good way to lose marks. (Formally, the three colours are interchangeable under a relabelling symmetry, so no value-ordering heuristic that depends only on domain sizes can distinguish them.)
Forward checking after SA = red: WA, NT, Q, NSW, V all become $\{$G, B$\}$. T stays $\{$R, G, B$\}$.
Step 3 — (c) The second variable
Domain sizes now: WA 2, NT 2, Q 2, NSW 2, V 2, T 3. MRV eliminates T immediately — it is the most flexible variable, so it goes last, which is correct: T is an independent subproblem and assigning it early tells us nothing.
Five-way tie at size 2, broken by degree among unassigned variables (SA no longer counts, since it is assigned):
| Variable | WA | NT | Q | NSW | V |
|---|---|---|---|---|---|
| remaining degree | 1 (NT) | 2 (WA, Q) | 2 (NT, NSW) | 2 (Q, V) | 1 (NSW) |
NT, Q and NSW tie at remaining degree 2; take NT (any of the three is a defensible answer, and a good solution says so).
Step 4 — (d) LCV for NT
$D_{\text{NT}} = \{$G, B$\}$. NT's unassigned neighbours are WA $\{$G,B$\}$ and Q $\{$G,B$\}$ (SA is assigned, so it does not count).
- NT = green: deletes G from WA (1) and G from Q (1) → 2 deletions.
- NT = blue: deletes B from WA (1) and B from Q (1) → 2 deletions.
Tied again, for the same symmetry reason. Take green.
Answer & the takeaway
(a) SA, by MRV with the degree tie-break (degree 5). (b) Any colour — LCV ties; say red. (c) NT, Q or NSW, all tied at domain size 2 and remaining degree 2; say NT. (d) Green or blue, tied; say green.
Takeaways. First: MRV did real work — it identified SA, the variable whose failure would otherwise be discovered 243 times over. Second: on a symmetric instance LCV frequently ties, and a correct answer says it ties rather than manufacturing a preference. LCV earns its keep once filtering has made the domains uneven, as in the worked example above where it separated red from blue for NSW. Third: notice that MRV and the degree heuristic pushed T to the very end — exactly right, since T is independent of everything.
18. Summary
The chapter in five lines:
- CSPs are a special kind of search problem: states are partial assignments, and the goal test is defined by constraints over subsets of the variables rather than being an opaque function of the whole state.
- The basic solution is backtracking search = DFS + one variable at a time + fail-on-violation.
- Speed-up 1, filtering — detect inevitable failure early: forward checking (cross values off the neighbours of the variable you just assigned), and arc consistency / AC-3 (make every arc consistent by deleting from the tail, propagating until the queue drains).
- Speed-up 2, ordering — MRV for variables (fail fast: fewest values left), LCV for values (succeed fast: rules out the least).
- Speed-up 3, structure — exploit the shape of the constraint graph.
| Technique | What it does | Cost per node | Detects failure |
|---|---|---|---|
| Naive DFS | checks constraints only at depth $n$ | $O(1)$ | at the very bottom |
| Backtracking | checks each new value against the assigned variables | $O(\deg \cdot 1)$ | when the conflicting variable is reached |
| + Forward checking | crosses the value off unassigned neighbours | $O(\deg \cdot d)$ | when a neighbour's domain empties |
| + AC-3 | makes every arc consistent, with propagation | $O(n^2d^3)$ | strictly earlier than forward checking |
| + MRV / LCV | reorders the branching | $O(n)$ / $O(\deg \cdot d)$ | as high in the tree as possible |
Structure, and what is left for later
The lecture lists structure as the third family of speed-ups and puts it in parentheses: mentioned, not covered. beyond syllabus Independent subproblems — solve each connected component separately. Tree-structured CSPs — $\mathbf{O(n d^2)}$ with no backtracking: root the tree, REVISE each parent→child arc from the leaves up, then assign forwards, every value guaranteed to extend (Guided problem 2 was an instance, which is why arc consistency alone solved it). Cutset conditioning — enumerate all $d^{|S|}$ assignments to a cycle cutset $S$ whose removal leaves a tree and solve that tree in $O(nd^2)$ each time, total $O(d^{|S|} \cdot n d^2)$; for Australia removing SA alone leaves a path plus isolated T, so $|S| = 1$ — the formal reason SA kept showing up as the bottleneck. Tree decomposition — $O(n d^{\,w+1})$ for treewidth $w$.
Also deferred: local search for CSPs (min-conflicts hill climbing — start from a complete, broken assignment and repair it; this is what really solves million-queens), continuous and infinite-domain CSPs (§8, handed to 07-380), constrained optimisation for the soft constraints of §9, which needs branch-and-bound rather than "stop at the first solution", and specialised propagators such as the alldiff algorithm §7's pigeonhole example calls for.
Next, in Chapter 5, the problem changes shape again: another agent gets to choose some of the values, and it is not on your side.