Constraint satisfaction: backtracking, constraint propagation and AC-3 on sudoku and a map of Czech regions
Variables, domains and constraints, the constraint graph, backtracking, the MRV and LCV heuristics, forward checking, AC-3 and min-conflicts — on colouring the regions of Czechia, sudoku and eight queens.
Series AI basics Part 23 of 52
- Map of artificial intelligence: what is what and how it all fits together
- A brief history of artificial intelligence: from Turing to language models
- Data, model, training, inference: the basic terms on a single example
- How to read mathematical notation: symbols, indices, sums and quantifiers
- Sets and relations: the building blocks of definitions in AI
- Functions and mappings: why every AI model is a function
- Logic and proofs: the rules AI reasons by
- Graphs and trees: the structure that both search and neural networks stand on
- Linear algebra for machine learning: vectors, matrices and why AI mostly multiplies
- Derivatives, the gradient and the chain rule: how a model finds out which way to learn
- Gradient descent: how a model learns
- What probability is: from rolling dice to the Kolmogorov axioms
- Conditional probability and Bayes’ theorem: how to update your estimate when you learn something new
- Random variables and the cumulative distribution function: one function that tells you everything about a distribution
- Probability density and probability mass functions: why a continuous variable has zero probability at a point
- Expected value, variance and covariance: where a distribution lies, how wide it is and what goes with what
- Overview of probability distributions: Bernoulli, binomial, Poisson, uniform, normal and exponential
- Law of large numbers and central limit theorem: why averages behave so predictably
- Statistics and parameter estimation: maximum likelihood, MAP and confidence intervals
- Information theory: entropy, cross-entropy and KL divergence — where loss functions come from
- State-space search: BFS, DFS, UCS and A* with animations
- Games and adversaries: minimax, alpha-beta pruning and Monte Carlo tree search
- Constraint satisfaction: backtracking, constraint propagation and AC-3 on sudoku and a map of Czech regions
- Logic and knowledge systems: forward and backward chaining, resolution and expert systems
- Planning: STRIPS, the blocks world, heuristics from relaxation and planning in AI agents
- Local search and simulated annealing: a travelling salesman through the Czech regional capitals
- Genetic algorithms: population, crossover, mutation and selection on a backpack for a hike
- Evolution strategies and genetic programming: from the one-fifth rule to Kepler’s law
- Ant colony optimization: how a pheromone trail finds a short path
- Particle swarm optimisation: inertia, personal experience and the swarm’s experience
- Neuroevolution: evolving the weights and shape of neural networks, and the NEAT algorithm
- Fuzzy sets and membership functions: how to write down “somewhat warm”
- Fuzzy logic and probability: why “somewhat warm” does not mean “probably warm”
- Fuzzy logic: t-norms, implications and IF–THEN rules
- Fuzzy control: Mamdani and Sugeno from a fan to a pole on a cart
- Fuzzy clustering: the fuzzy c-means algorithm and points between clusters
- Neuro-fuzzy systems (ANFIS): fuzzy rules that learn from data
- Markov chains: the transition matrix, the stationary distribution and PageRank
- Hidden Markov models: the Viterbi algorithm and the dishonest casino
- Bayesian networks: why the grass is wet and what it means for the sprinkler
- The Kalman filter: how to estimate where a car is and how fast it goes from imprecise measurements
- Types of machine learning: supervised, unsupervised, semi-supervised and more
- Prompts, context and RAG: how to give a model the right information
- Variational autoencoders (VAE): new images from a continuous latent space
- Generative adversarial networks (GAN): the forger versus the detective
- Diffusion models: how pure noise becomes an image
- Markov decision processes: states, actions, rewards and the Bellman equations
- Q-learning and DQN: learning the value of actions, from a table to a neural network
- Policy gradient and PPO: learning the policy directly
- How an AI agent works: a language model in a loop with tools
- Programming with AI agents: the task, tests and review
- Evaluating and securing AI systems: how to measure and how to keep watch
Full series syllabus 52 of 83 topics published
- How to write a timetable, a sudoku or map colouring as variables, domains and constraints.
- How backtracking with heuristics and forward checking cuts the search from millions of steps to dozens.
- How AC-3 propagates deletions through the constraint graph and why min-conflicts can handle even a million queens.
Useful to know first: State-space search: BFS, DFS, UCS and A* with animations, Graphs and trees: the structure that both search and neural networks stand on
A timetable in which no teacher teaches two classes at once. Sudoku. Placing components on a circuit board, hospital shifts, assigning aircraft to gates. What all these problems have in common is that we are not looking for a path, but for an assignment: giving each variable a value so that all the rules hold at once. Such a problem is called a constraint satisfaction problem, and unlike general search it has one big advantage — the rules are visible, so the algorithm can reason from them, not just try things.
It builds on the parts State-space search and Graphs and trees.
1. What a constraint satisfaction problem is
Definition — CSP
Let n ∈ ℕ, n ≥ 1. A constraint satisfaction problem (CSP) is a triple (X, D, C), where
- X = {X1, …, Xn} is a set of n distinct variables;
- D = (D1, …, Dn), where for every i = 1, …, n, Di is a non-empty set — the domain of the variable Xi, i.e. the set of values Xi may take (in this part all domains are finite);
- C is a finite set of constraints. Each constraint is a pair (S, R), where the scope S = (Xi1, …, Xir) is an r-tuple of pairwise distinct variables from X (r ≥ 1) and the relation R ⊆ Di1 × … × Dir is the set of allowed combinations of their values.
The number r is the arity of the constraint. A constraint with r = 2 is binary; for example the constraint X1 ≠ X2 is the pair ((X1, X2), {(x, y) ∈ D1 × D2 : x ≠ y}). A CSP is binary if and only if all its constraints are binary. Where convenient, we will also treat the scope S as the set of its variables (S ⊆ X).
In words: a CSP consists of finitely many variables, a set of admissible values for each, and a list of rules, each of which enumerates the allowed combinations of values of several variables.
Variables are the questions you are looking for answers to (“what colour does this region get?”). The domain is the possible answers (“red, green, blue”). Constraints are the rules the answers must obey (“neighbouring regions must not have the same colour”).
Definition — assignment and solution
Let (X, D, C) be a CSP. An assignment is a function a : dom(a) → D1 ∪ … ∪ Dn, where dom(a) ⊆ X, such that
a(Xi) ∈ Di for every Xi ∈ dom(a).
For a constraint (S, R) ∈ C with scope S = (Xi1, …, Xir) ⊆ dom(a), let a|S = (a(Xi1), …, a(Xir)) denote the values of the variables of the scope. An assignment a is
- consistent if and only if a|S ∈ R for every constraint (S, R) ∈ C with S ⊆ dom(a);
- complete if and only if dom(a) = X.
A solution of a CSP is an assignment that is complete and consistent. In words: a solution gives every variable a value and violates no constraint; a consistent assignment violates no constraint whose variables all have a value already.
A sudoku in progress in which nothing clashes yet is a consistent but incomplete assignment. A filled-in sudoku in which nothing clashes is a solution.
An assignment a is thus a function from the set of variables dom(a) ⊆ X to their domains. The consistency condition from the definition can be written as a single expression:
a is consistent ⇔ ∀(S, R) ∈ C : S ⊆ dom(a) ⇒ a|S ∈ R
- ∀(S, R) ∈ C
- we go through every constraint — the scope S and the relation R; a single violated one is enough and the assignment is wrong
- S ⊆ dom(a)
- we judge only constraints whose variables all have a value already; a constraint with a free variable cannot be decided yet, and therefore a partial assignment cannot violate it
- a|S ∈ R
- the values of the variables in the scope (the restriction of a to S) must form a combination the relation allows
Example — colouring the regions as a CSP
X = {KVK, ULK, …, MSK} are the 14 regions, each has the domain Di = {1, 2, 3, 4} (colour numbers), and C contains one constraint ((Xi, Xj), ≠) for each of the 26 shared borders, i.e. the relation {(x, y) : x ≠ y} with twelve allowed pairs out of 16.
There are |D1| · |D2| · … · |D14| = 414 = 268,435,456 complete assignments. Among them we are looking for those that satisfy all 26 constraints.
Why not treat it as ordinary search? We could — the state is a partial assignment, the action is “give the next variable a value”. But search would look at the state as a black box. A CSP sees inside the box: it knows which rules hold and which variables they concern. All the tricks in this part rest on that.
2. The constraint graph: colouring a map of regions
A classic problem: colour a map so that no two neighbouring countries have the same colour. We take the fourteen regions of Czechia. The variables are the regions, the domain is a set of colours, and for each shared border there is the constraint “different colours”.
Definition — constraint graph
Let (X, D, C) be a binary CSP with variables X = {X1, …, Xn}. Its constraint graph is the undirected graph (Graphs and trees) G = (V, E), where
V = X, E = {{Xi, Xj} : i ≠ j ∧ ∃R : ((Xi, Xj), R) ∈ C}.
The variables Xi and Xj are thus joined by an edge if and only if there is a constraint whose scope consists of exactly these two variables (in either order — which is why it is enough to go through i, j both ways). The neighbours of the variable Xi in this graph are denoted N(Xi) = {Xj : {Xi, Xj} ∈ E}.
Each region is a circle, and a line between two circles means “these two are neighbours, they must not have the same colour”. All that is left of the map is what matters: who borders whom.
With four colours it goes smoothly. With three it does not work at all — and the graph shows why. The Central Bohemian Region borders Prague and seven regions that form a closed ring around it. A ring with an odd number of vertices needs three colours on its own (two would have to alternate regularly, and with an odd number that does not work out). The Central Bohemian Region borders all seven, so a fourth colour has to be left for it:
Theorem — three colours are not enough for the regions
Let G = (V, E) be a finite undirected graph and k ∈ ℕ, k ≥ 1. A colouring of the graph G with k colours is a function c : V → {1, …, k} such that
∀{u, v} ∈ E : c(u) ≠ c(v).
The chromatic number of the graph G is
χ(G) = min {k ∈ ℕ, k ≥ 1 : a colouring of G with k colours exists},
i.e. the smallest number of colours with which the vertices can be coloured so that every two neighbouring vertices have different colours (for a finite graph the minimum exists; |V| colours are always enough).
Lemma. Let G = (V, E) be a graph, m ≥ 3 odd and v1, …, vm ∈ V vertices forming a cycle, i.e. {vi, vi+1} ∈ E for i = 1, …, m − 1 and {vm, v1} ∈ E. Then there is no function c : {v1, …, vm} → {1, 2} that gives neighbours different colours on these edges. Proof: with two colours, the colours must alternate along the cycle; if v1 has colour 1, then vi has colour 1 exactly for odd i. Then vm (odd m) has colour 1, just like its neighbour v1. A contradiction. ∎
Theorem. Let G be the graph of regions: the vertices are the regions of Czechia (14 vertices) and {u, v} ∈ E if and only if the regions u and v share a border. Then χ(G) = 4. Proof: a colouring with four colours exists (figure above), so χ(G) ≤ 4. Suppose there were a colouring c with three colours. The ring of 7 regions around Central Bohemia is a cycle of odd length, so by the lemma c cannot make do with fewer than three colours on it — it uses all three. The Central Bohemian Region borders every region of the ring, and c(STČ) therefore differs from all three colours, which is impossible in {1, 2, 3}. A contradiction, so χ(G) ≥ 4. ∎
That four colours are enough for every map is the famous four colour theorem. It was proved by Appel and Haken in 1976 — as the first major mathematical theorem proved with the help of a computer, which checked almost two thousand cases.
3. Backtracking
Definition — backtracking
Backtracking is depth-first search that in each step assigns a value to one more variable. For an assignment a, a variable Xi ∉ dom(a) and a value v ∈ Di, we write a ∪ {Xi ↦ v} for the assignment a extended by the value v of the variable Xi.
Input: a CSP (X, D, C) with finite domains and a consistent assignment a (empty at the start, dom(a) = ∅).
Output: a solution of the CSP that extends a, or “failure” if no such solution exists.
The procedure BT(a):
- If dom(a) = X, return a (a complete consistent assignment is a solution).
- Pick a variable Xi ∈ X ∖ dom(a).
- For each value v ∈ Di (in a fixed order): if a ∪ {Xi ↦ v} is consistent, call BT(a ∪ {Xi ↦ v}); if that call returns a solution, return it. An inconsistent value is not tried at all.
- If no value led to a solution, return “failure” — the caller thereby goes back to the previous variable and tries its next value.
Termination: each nested call has one more assigned variable, so the depth of the recursion is at most n = |X|, and each call tries at most |Di| values. The call tree is therefore finite and the algorithm always terminates.
Let d = maxi |Di|. In the worst case backtracking goes through O(dn) assignments. The general CSP is NP-hard, so in the worst case we cannot do better.
You fill in square by square. As soon as something does not fit, you do not continue — you go back a step, change the last decision and keep trying. Like DFS from the previous part, just with one small detail that makes a huge difference: the order of the variables does not matter for the result, so it is enough to decide about one variable per level of the tree.
def backtracking(assignment, csp):
if len(assignment) == len(csp.variables):
return assignment # everything has a value: a solution
p = select_variable(assignment, csp)
for h in csp.domains[p]:
if consistent(p, h, assignment, csp):
assignment[p] = h
result = backtracking(assignment, csp)
if result:
return result
del assignment[p] # dead end: go back
return None
Why does “one variable per level” make a huge difference? General search would offer, in every state, all pairs (unassigned variable, value). The same complete assignment could then be put together in every possible order, and the tree would have
n! · dn leaves instead of dn
- n!
- the number of orders in which n variables can be assigned; the result does not depend on the order (an assignment is a set of pairs), so all but one are useless copies of the same work — backtracking throws them away by deciding about a single variable on each level
- dn
- the number of different complete assignments: each of the n variables independently takes one of d values; this many leaves remain even if backtracking pruned nothing
On the regions with four colours, backtracking makes 14 assignments, one per region, and not once does it go back. With three colours it has to prove that no solution exists — and that means trying everything: 93 assignments before it gives up. The beginning of its run (the indentation is the depth in the tree, the colours are the numbers 1–3, ✗ means that no colour was left for the region):
KVK = 1
ULK = 2
PLK = 3
PHA = 1
STČ ✗
PHA = 2
STČ = 1
LBK = 3
JHČ = 2
HKK = 2
VYS = 3
PAK ✗
PHA = 3
STČ = 1
…
The very first dead end is instructive: the Ústí nad Labem Region, the Plzeň Region and Prague took all three colours from the Central Bohemian Region. Backtracking, however, goes back only to the last variable — to Prague — and recolours it, even though the real trouble lies in the odd ring. So Prague gets recoloured again and again during the run. This helpless marking time is called thrashing, and a smarter choice of variables cuts it down considerably.
4. Which variable and which value
The skeleton of backtracking leaves two questions open: which variable to take next and in what order to try its values. The answers matter more than it might seem.
Definition — MRV, degree, LCV
Let (X, D, C) be a binary CSP with constraint graph G and neighbours N(·), let a be a consistent assignment and U = X ∖ dom(a) ≠ ∅ the set of unassigned variables. For Xi ∈ U, denote the remaining values
D′i = {v ∈ Di : a ∪ {Xi ↦ v} is consistent}.
- MRV (minimum remaining values) picks as the next variable one of M = {Xi ∈ U : |D′i| = minXk ∈ U |D′k|}, i.e. the variable with the fewest values that are not yet in conflict with the assignment.
- The degree heuristic breaks a tie: from M it picks the variable Xi with the largest |N(Xi) ∩ U|, i.e. with the most constraints with variables not yet assigned.
- LCV (least constraining value): for the chosen Xi, v ∈ D′i and every Xj ∈ N(Xi) ∩ U, let D″j(v) = {w ∈ D′j : a ∪ {Xi ↦ v, Xj ↦ w} is consistent} and loss(v) = ∑Xj ∈ N(Xi) ∩ U |D′j ∖ D″j(v)|. LCV tries the values v ∈ D′i in ascending order of loss(v).
In words: MRV takes the variable with the fewest options left, the degree heuristic on a tie the one that constrains the others the most, and LCV first tries the value that crosses out the fewest options for its neighbours. For the constraint ≠ we have D′j ∖ D″j(v) = {v} ∩ D′j, so the loss is simply the number of unassigned neighbours that still have v left.
For the variable, take the hardest one: fill in a sudoku square where only one number still fits right away — and if nothing fits there, you find out before you build another twenty levels on top of the mistake. For the value, on the other hand, take the most accommodating one, which leaves the others the most room. You choose the variable so as to fail fast, the value so as to succeed.
For three colours on the regions, MRV together with forward checking from the next section reduces the number of assignments from 93 to 45 — it starts with the region that has the fewest colours left, and finds the contradiction twice as fast.
On an empty map all regions have three colours each and MRV does not decide. The degree heuristic would then take the Central Bohemian Region, which has 8 neighbours — the most of all; its colour immediately constrains eight other regions. Our implementation simply takes, on a tie, the first region in order from the west, i.e. the Karlovy Vary Region.
5. Forward checking
Definition — forward checking
Let (X, D, C) be a binary CSP in which every two variables are joined by at most one constraint (several constraints between the same pair can be replaced by one with the intersection of the relations). For a constraint ((Xi, Xj), R) ∈ C we write Rij = R and Rji = {(y, x) : (x, y) ∈ R}.
Let a be a consistent assignment, D1, …, Dn the current (possibly already narrowed) domains, Xi ∉ dom(a) and v ∈ Di. Forward checking after the assignment Xi ↦ v sets Di ← {v} and, for every variable Xj ∈ N(Xi) ∖ dom(a),
Dj ← {w ∈ Dj : (v, w) ∈ Rij};
it does not change the other domains. If after narrowing Dj = ∅ for some j, the assignment Xi ↦ v is withdrawn immediately — it cannot be extended to a solution, because Xj would have no admissible value.
In words: after every assignment, the values incompatible with the assigned value are crossed out of all unassigned neighbours, and an empty domain means an immediate step back.
You write a seven into a sudoku and immediately cross it out of the pencil marks of all the squares in the same row, column and box. If that left some square with nothing, you know the seven was a mistake — even before you move on to the next square.
def forward_check(csp, domains, assignment, p, h):
new = dict(domains)
new[p] = [h]
for s in csp.neighbors[p]:
if s in assignment:
continue
new[s] = [x for x in new[s] if csp.allowed(p, h, s, x)]
if not new[s]:
return None # the neighbour has nothing left: withdraw p = h
return new
Backtracking then passes the narrowed new domains into the recursion instead of the original ones, and MRV
only compares their lengths. This is what the beginning of the run with forward checking and MRV
looks like on the regions with three colours. The right-hand column shows the unassigned regions with the smallest
domain — MRV picks the first of them:
| step | assignment | neighbours’ domains after crossing out | smallest domains |
|---|---|---|---|
| 1 | KVK = 1 | PLK {2, 3}, ULK {2, 3} | ULK, PLK (2) |
| 2 | ULK = 2 | PLK {3}, STČ {1, 3}, LBK {1, 3} | PLK (1) |
| 3 | PLK = 3 | STČ {1}, JHČ {1, 2} | STČ (1) |
| 4 | STČ = 1 | PHA {2, 3}, LBK {3}, HKK {2, 3}, PAK {2, 3}, VYS {2, 3}, JHČ {2} | LBK, JHČ (1) |
| 5 | LBK = 3 | HKK {2} | JHČ, HKK (1) |
| 6 | JHČ = 2 | VYS {3}, JHM {1, 3} | HKK, VYS (1) |
| 7 | HKK = 2 | PAK {3} | VYS, PAK (1) |
| 8 | VYS = 3 | PAK ∅, JHM {1} | PAK = ∅ |
After the first three steps the Central Bohemian Region has a single colour left, so it gets its turn before Prague. Every further region chosen has only one option left, and in the eighth step forward checking finds that the Pardubice Region has nothing left — the contradiction is uncovered without the Pardubice Region even being tried. The whole ring is thus forced by itself; a free decision was made only for the first two regions.
The difference shows on the sudoku below. Plain backtracking, square by square along the rows, needs over 14 million assignments. With forward checking it takes 11,400. And when we add MRV to it, it drops to 81 — 30 given numbers and 51 filled in, without a single backtrack.
6. Arc consistency and AC-3
Forward checking looks only one step ahead: at the neighbours of the variable we have just assigned. It can be done more thoroughly — crossing out even between variables that do not have a value yet.
Definition — arc consistency
Let (X, D, C) be a binary CSP with the notation Rij from the definition of forward checking, and let Xi, Xj ∈ X be variables joined by a constraint, i.e. {Xi, Xj} ∈ E. The variable Xi is arc-consistent with respect to Xj if and only if
∀x ∈ Di ∃y ∈ Dj : (x, y) ∈ Rij.
An ordered pair (Xi, Xj) with {Xi, Xj} ∈ E is called a directed edge; every edge of the constraint graph gives two. A CSP is arc-consistent if and only if Xi is arc-consistent with respect to Xj for every directed edge (Xi, Xj). In words: every value in the domain of every variable has, at every neighbour, at least one value with which it satisfies the constraint.
Every value must have at least one “partner” at the neighbour that it can live with. A value without a partner is dead — it will not appear in any solution — and you can safely cross it out.
Making one directed edge consistent (the operation revise in the code below) means
keeping only the values that have a partner:
Di ← { x ∈ Di : ∃y ∈ Dj : (x, y) ∈ Rij }
- Di ←
- we overwrite the domain in place; only the domain of Xi shrinks, the edge is directed — the opposite direction (Xj, Xi) is a separate item in the queue
- x ∈ Di
- we judge each value separately; we add nothing new, so the domain can only shrink — which is why AC-3 terminates
- ∃y ∈ Dj
- one partner in the neighbour’s current domain is enough; we ask whether x can survive, not how
- (x, y) ∈ Rij
- the pair must satisfy the constraint between Xi and Xj — for map colouring simply x ≠ y
Algorithm — AC-3
Input: a binary CSP (X, D, C) with finite domains and constraint graph G = (X, E).
Output: “false” if some domain was left empty (the CSP then has no solution); otherwise “true” and domains narrowed so that the CSP is arc-consistent and has the same set of solutions as on input.
- Put all directed edges (Xi, Xj), {Xi, Xj} ∈ E, into a queue Q.
- If Q is empty, stop with the result “true”.
- Take an edge (Xi, Xj) out of Q and perform Di ← {x ∈ Di : ∃y ∈ Dj : (x, y) ∈ Rij} (breakdown above).
- If Di has shrunk: if Di = ∅, stop with the result “false”; otherwise put into Q all edges (Xk, Xi) for Xk ∈ N(Xi), Xk ≠ Xj — their values may have just lost a partner.
- Continue with step 2.
Termination: edges return to the queue only when some domain shrinks; the domains are finite and never grow, so after finitely many steps the queue is empty.
For c binary constraints and domains of size at most d, AC-3 takes O(c · d3).
The crossing out spreads like a wave: when a square loses an option, its neighbours have to take another look too, because they may have lost a partner. The wave stops when there is nothing left to cross out. This is called constraint propagation.
from collections import deque
def ac3(csp):
queue = deque((xi, xj) for xi in csp.variables for xj in csp.neighbors[xi])
while queue:
xi, xj = queue.popleft()
if revise(csp, xi, xj):
if not csp.domains[xi]:
return False # empty domain: no solution exists
for xk in csp.neighbors[xi]:
if xk != xj:
queue.append((xk, xi))
return True
def revise(csp, xi, xj):
before = len(csp.domains[xi])
csp.domains[xi] = [x for x in csp.domains[xi]
if any(csp.allowed(xi, x, xj, y) for y in csp.domains[xj])]
return len(csp.domains[xi]) < before
AC-3 step by step
Three variables X, Y, Z, every two of which must differ, and the domains DX = {1}, DY = {1, 2}, DZ = {1, 2, 3}. The queue starts with six directed edges; a row of the table is one removal from the queue and one narrowing Di ← by the formula above (the column “domain after” has the colour of the same term):
| step | edge | crossed out | domain after | into the queue | in the queue |
|---|---|---|---|---|---|
| 1 | (X, Y) | — | {1} | — | 5 |
| 2 | (X, Z) | — | {1} | — | 4 |
| 3 | (Y, X) | 1 | {2} | (Z, Y) | 4 |
| 4 | (Y, Z) | — | {2} | — | 3 |
| 5 | (Z, X) | 1 | {2, 3} | (Y, Z) | 3 |
| 6 | (Z, Y) | 2 | {3} | (X, Z) | 3 |
| 7 | (Z, Y) | — | {3} | — | 2 |
| 8 | (Y, Z) | — | {2} | — | 1 |
| 9 | (X, Z) | — | {1} | — | 0 |
In step 3 Y loses the one, because its only possible partner at X is exactly 1. This puts the edge (Z, Y) back into the queue — the value 2 in Z had its partner in exactly that crossed-out one. The value 2 is, however, crossed out of Z already by the original edge (Z, Y) in step 6; the returned copy in step 7 finds nothing. (Our queue is not a set, so an edge can be in it twice — that changes nothing about the result.) After 9 steps the queue is empty and the domains are {1}, {2}, {3}: a solution without a single attempt.
O( 2c · d · d2 ) = O(c · d3)
- 2c
- every binary constraint gives two directed edges, and each of them has to be checked
- d
- how many times at most the edge (Xk, Xi) returns to the queue: only when Di shrinks, and that happens at most d times
- d2
- the cost of one check of an edge: for each of at most d values x we look for a partner among at most d values y
7. Sudoku: propagation in practice
Sudoku is a textbook CSP: 81 variables, domains {1, …, 9} and the constraint “different values” for every pair of squares in the same row, column or 3 × 3 box. Each square thus has 20 neighbours in the constraint graph.
Example — sudoku in numbers
Neighbours of a square: 8 others in the row + 8 in the column + 4 squares of the 3 × 3 box that lie in neither its row nor its column (the remaining 4 we have already counted) = 20.
Constraints: 81 · 20 = 1620 directed edges; every pair of squares is in there twice, so there are 1620/2 = 810 binary constraints.
Size of the domains at the start: 30 given squares with the domain {given number} and 51 empty ones with nine values: 30 · 1 + 51 · 9 = 489 values. At the end each of the 81 squares has one value, so AC-3 crossed out 489 − 81 = 408 values. In doing so it took 9,372 edges from the queue.
Brute force: trying every way of filling in the empty squares means 951 ≈ 4.6 · 1048 assignments (dn with d = 9 and n = 51 empty squares; colour as in the formula in the Backtracking section). Thanks to the consistency check, backtracking makes “only” fourteen million of them.
Let us run AC-3 alone on a puzzle with 30 given numbers, without a single blind assignment. It crosses out 408 values — and each of the 51 empty squares is left with exactly one. The sudoku is solved by pure reasoning:
That is how it goes with easy sudokus; with harder ones propagation gets stuck somewhere and search takes over — backtracking that runs propagation again after each assignment. This combination (maintaining arc consistency, MAC) is the basis of most real solvers. For comparison, on the same puzzle:
| Algorithm | Assignments | Backtracks |
|---|---|---|
| Backtracking along the rows | 14,740,819 | 14,740,738 |
| + forward checking | 11,400 | 11,319 |
| + forward checking + MRV | 81 | 0 |
| AC-3 without search | 0 | 0 |
From fourteen million to zero. Brute force and clever reasoning solve the same problem — only one of them reads the rules.
8. Local search: min-conflicts
A completely different strategy: instead of building the solution piece by piece, start with a complete assignment that violates plenty of constraints, and gradually repair it. It is the local search from the part on search, only the objective function is the number of violated constraints.
Algorithm — min-conflicts
Input: a CSP (X, D, C) with finite domains and a maximum number of steps kmax ∈ ℕ.
Output: a solution of the CSP, or “failure”.
For a complete assignment a, a variable Xi and a value v ∈ Di, let conflicts(Xi, v) be the number of constraints (S, R) ∈ C with Xi ∈ S violated by the assignment obtained from a by changing the value of Xi to v. The variable Xi is in conflict if and only if conflicts(Xi, a(Xi)) > 0.
- Choose a random complete assignment a (a(Xi) ∈ Di for all i).
- Repeat at most kmax times: if a is consistent (no variable is in conflict), return a. Otherwise pick a random variable Xi in conflict and set a(Xi) ← v, where v ∈ Di has the smallest conflicts(Xi, v) (ties broken randomly) — i.e. the value with which it violates the fewest constraints.
- After kmax steps without a solution, return “failure”.
Termination: after kmax steps at the latest. The algorithm is not complete: “failure” does not mean that no solution exists.
You place everything at once, however it falls, and then you repair: you take one thing that is quarrelling with something and move it to where it quarrels the least. You remember no tree and no history — just the current state and what is grinding in it.
The classic test is the n-queens problem: place n chess queens on an n × n board so that no two attack each other. The variable is the row of the queen in each column. For eight queens our run started with six attacking pairs and finished after 8 steps:
What exactly does the algorithm minimise? The queen in column i stands in row ri. The number of conflicts if it were placed in row v is
conflicts(i, v) = ∑j ≠ i [ rj = v ∨ |rj − v| = |j − i| ]
- conflicts(i, v)
- the cost of the move “queen from column i to row v”; min-conflicts picks the v for which it is smallest
- ∑j ≠ i [ … ]
- we add up over all the other queens; the square bracket is 1 when the condition holds, otherwise 0 — we count violated constraints
- rj = v
- queen j stands in the same row; it cannot share a column, the choice of variables (one queen per column) already rules that out
- |rj − v| = |j − i|
- the queens lie on the same diagonal: the shift in rows equals the shift in columns
def min_conflicts(csp, max_steps):
a = random_complete_assignment(csp)
for _ in range(max_steps):
in_conflict = [x for x in csp.variables if conflicts(csp, a, x, a[x]) > 0]
if not in_conflict:
return a # nothing is grinding: a solution
x = random.choice(in_conflict)
costs = {v: conflicts(csp, a, x, v) for v in csp.domains[x]}
fewest = min(costs.values())
a[x] = random.choice([v for v in costs if costs[v] == fewest])
return None # patience ran out
Our run move by move. We number both columns and rows 1–8 (row 1 at the top); the “costs” are the values conflicts(i, v) for v = 1, …, 8, and the smallest of them is in bold (their colour corresponds to the term conflicts(i, v) in the formula above). At the start the queens stood in rows 5 1 6 2 5 1 1 8.
| step | column | costs for rows 1–8 | row | conflicts after the move |
|---|---|---|---|---|
| 1 | 8 | 4 3 1 0 2 2 1 1 | 8 → 4 | 5 |
| 2 | 7 | 2 3 2 1 4 2 1 0 | 1 → 8 | 3 |
| 3 | 1 | 3 3 0 2 2 2 0 2 | 5 → 3 | 1 |
| 4 | 2 | 1 3 2 3 3 1 1 2 | 1 → 7 | 1 |
| 5 | 3 | 3 1 3 3 2 1 2 2 | 6 → 2 | 1 |
| 6 | 3 | 3 1 3 3 2 1 2 2 | 2 → 2 | 1 |
| 7 | 4 | 2 1 3 2 3 2 1 2 | 2 → 7 | 1 |
| 8 | 2 | 2 3 3 2 3 0 1 2 | 7 → 6 | 0 |
In step 6 the algorithm drew, out of two equally good rows, the one in which the queen already stood — the move changed nothing. In steps 4 to 7 the smallest cost is 1; only in the eighth is a row with zero found in the drawn column.
Notice the middle steps: the number of conflicts stays at one for four steps — the algorithm moves queens “sideways” along a plateau until it finds a move that removes the conflict. What is surprising is how well it scales: min-conflicts solves a million queens in roughly fifty steps on average. The same method was used to schedule observations for the Hubble Space Telescope (both are described by Minton, Johnston, Philips and Laird, 1992).
9. Constraints in practice
- Timetables and planning. School timetables, shifts, classroom allocation and production plans are specified as variables, domains and constraints. Solvers such as Google OR-Tools combine propagation, search and local improvement.
- SAT solvers solve CSPs in which every variable is true/false. They verify chip designs, look for bugs in software, and package managers (for example in Linux) use them to put together versions of libraries that work with each other.
- Compilers allocate a program’s variables to processor registers by graph colouring: two variables that live at the same time must not share a register — exactly like neighbouring regions a colour.
- Configurators in online shops and manufacturing make sure the selected components go together.
- AI agents and language models often do not excel at CSPs: for a sudoku or a timetable a solver is more reliable. A good agent therefore does not think the problem through “in its head” — it translates it into variables and constraints and calls a tool on it.
10. Summary
- A CSP is a triple (X, D, C): variables, domains and constraints. A solution is a complete assignment that violates no constraint.
- Backtracking assigns variables one after another and backs out of dead ends; in the worst case it is exponential.
- The MRV and LCV heuristics choose the hardest variable and the most accommodating value; forward checking uncovers a dead end one step earlier.
- AC-3 propagates crossing out through the constraint graph until every value has a partner; it solves an easy sudoku entirely without search.
- Min-conflicts repairs a complete assignment and copes even with huge problems, such as a million queens.
Want to get hands-on? Have an AI agent write a sudoku solver with backtracking, and then add forward checking, MRV and AC-3 to it — and count how many assignments disappear.
Do you see it differently? Write to support@coden-app.com.