Filtering is not enough


It didn’t build a solution

although all impossible values were removed


It seems that we need to dive into the

search space 🤿

in a DFS way.


Adapting the DFS

  • If all variables are fixed, stop the search
  • If inconsistency detected, then backtrack
  • Expand the tree by making a decision$^*$


*: 2-way branching

Alt text.


Backtracking

For this, we need to retrieve the state of the domains before a decision is applied.

And therefore it must have been registered beforehand.

And we must be able to rebut a decision.


Search strategy

How to select the next decision to apply?

select an uninstantiated variable
pick a value in its domain (e.g., the lower bound)

A recursive approach should simplify our task

Let’s break down the needs into 4 functions.

def make_decision(variables):
    pass

def copy_domains(variables):
    pass

def apply_decision(variables, var, val, apply, constraint):
    pass

def dfs(variables, constraint):
    pass

>>🥛<<


def make_decision(variables):
    var, dom = next(
        filter(lambda x: len(x[1]) > 1, variables.items()),
        (None, None))
    if var is not None:
        # if true, returns the decision
        return (var, min(dom))
    else:
        # otherwise, all variables are instantiated
        return None

def copy_domains(variables):
    # returns a deep copy of the dictionnary
    return {var: dom.copy() for var, dom in variables.items()}

def apply_decision(variables, var, val, apply, constraint):
  # copy the domains
  c_variables = copy_domains(variables)
  # applies the decision or rebuts is
  c_variables[var] = {x for x in c_variables[var] \
                      if apply is (x == val)}
  # explore the sub-branch
  return dfs(c_variables, constraint)

def dfs(variables, constraint):
  constraint.filter(variables)
  dec = make_decision(variables)
  if dec is None:
      print(variables)  # prints the solution
      return 1
  else:
      var, val = dec
      n = apply_decision(variables, var, val, True, constraint)
      n += apply_decision(variables, var, val, False, constraint)
  return n

variables = {"x1": {1, 2, 3},
             "x2": {1, 2, 3}}
c1 = LessThan("x1", "x2")

print("There are", dfs(variables, c1), "solutions")

should output:

{'x1': {1}, 'x2': {2}}
{'x1': {1}, 'x2': {3}}
{'x1': {2}, 'x2': {3}}
There are 3 solutions

We did it !

Well almost, but still you can pat yourself back !


🚀 Managing backups

📄“Comparing trailing and copying for constraint programming”, C. Schulte, ICLP'99.

  • Copying: An identical copy of S is created before S is changed.
  • Trailing: Changes to S are recorded such that they can be undone later.
  • Recomputation: If needed, S is recomputed from scratch.
  • Adaptive recomputation: Compromise between Copying and Recomputation.

🚀 Making decisions

  • Dynamic criteria for variable selection
  • And value selection (min, max, mid, rnd)
  • Different operators ($=, \neq, \leq, \geq$)
  • Depth-first, limited-discrepancy, large neighborhood