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:

  • 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.
Link to original

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"

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 original
  • Most 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 original

    There 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. 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.

      Link to original

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 removed

Going 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?