In order to solve CSPs efficiently, we can continue to iterate on our search methodology.
Backtracking Search
Backtracking Search
Backtracking search is a variation of the Depth First Search in which we do:
Link to original
- Single-variable assignments Only assignment to one variable at each level of the search tree, this accounts for commutativity: e.g. assigning A to red then B to green is the same as B to green then A to red. This reduces paths from to .
- For-each assignment Check for failure. Backtracks on failure to next assignment. If all assignments fail, return failure.
Backtracking Procedure
Below describes a procedure for recursive backtracking:
def recursiveBacktracking(assignment, csp):
if assignment.complete:
return assignment
var = selectUnassignedVariable(variables[csp], assignment, csp)
for each value in orderDomainValues(var, assignment, csp):
if value is consistent with assignment given constraints[csp]:
assignment.add({ var = value })
result = recursiveBacktracking(assignment, csp)
if not result.failure:
return result
assignment.remove({ var = value })
throw "failure"Further improvements to backtracking search
However this is still not complete and it’s inefficient.
- We are still exhaustively trying all combinations until failure at a given level.
- There are an arbitrary selection of values.
To further improve the algorithm, we could:
- Decide which variable to assign at each level.
- Decide the order in which the values are tried.
- Try and detect failure early.
So what can we do to improve?
To decide which variable to assign next, we could look at:
Most Constrained Variable
Most Constrained Variable: choose the variable with the fewest legal values. “Minimum Remaining Values (MRV) heuristic”
Link to originalMost Constraining Variable
Most Constraining Variable: choose the variable that imposes the most constraints on the remaining values for other variables.
Link to original
Once we select a variable, we also need to figure out which value to pick:
Choose the least constraining value: value that rules out the fewest values in the remaining variables.
We can also detect failure early:
-
Forward Checking (Search)
Forward Checking: propagate information from assigned to unassigned variables. We keep track of remaining legal values for unassigned variables and terminate the search when any variable has no legal values.
Link to original
-
Constraint Propagation
Constraint Propagation: communicate domain reduction of a decision variable to all of the constraints stated over said variable.
It is often much faster than traditional state-space search. It can eliminate areas of the state space that are no longer relevant.
Link to originalThere are different forms of this such as:
Arc Consistency
Arc Consistency: keep pairs of variables consistent, is consistent iff for each value of there are allowed values of . When checking , throw out any values of for which there isn’t an allowed value of .

In this example, the blue value in NSW is thrown out.
If loses a value, all pairs need to be rechecked.
Link to original
Here we have now run into a situation where we can’t satisfy the problem.
So we’ve eliminated the original assignment we’ve chosen.
Arc Consistency Algorithm (AC-3)
Wikipedia: https://en.wikipedia.org/wiki/AC-3_algorithm Given a CSP, we try to reduce its domains with the following algorithm:
def AC-3(csp):
queue = csp.arcs
while queue is not empty:
(i, j) = removeFirst(queue)
if removeInconsistentValues(i, j):
for each k in neighbors[i]:
queue.push((k, i))
def removeInconsistentValues(i, j):
removed = False
for each x in domain[i]:
if no value y in domain[j] allows (x, y) to satisfy constain i <-> j:
delete x from domain[i]
removed = True
return removedGoing back to our backtracking search algorithm, we can implement it like so:
def recursiveBacktracking(assignment, csp):
if assignment.complete:
return assignment
var = selectUnassignedVariable(variables[csp], assignment, csp)
for each value in orderDomainValues(var, assignment, csp):
if value is consistent with assignment given constraints[csp]:
assignment.add({ var = value })
# run our inferences, such as AC-3
inferences = INFERENCE(csp, var, value)
if inferences do not fail:
assignment.add(inferences)
result = recursiveBacktracking(assignment, csp)
if not result.failure:
return result
assignment.remove({ var = value })
throw "failure"Relations
All of the examples we’ve looked at so far rely on unary constraints.
todo: program lion / unicorn example?