Weak-commitment Search (WCS) can be seen as one of the simplest form of nogood learning. It was proposed by Yokoo, and the main reference is:
Makoto Yokoo, Weak-commitment Search for Solving Constraint Satisfaction Problems, in AAAI'94, pg. 313-318.WCS starts by giving tentative assignments to the variables of the problem. Labelling of the variables are then performed, guided by a heuristic that considers the tentative values. If a dead-end is reached, where no value can be assigned to a variable without violating some constraint, then the current search is abandoned, and a new search restarted from scratch with an extra `nogood' constraint. This `nogood' constraint remembers the previously assigned values to the already labelled variables in the just abandoned search. This combination of values lead to a dead-end, and will not be tried again in the new search. This is in contrast to a conventional backtracking search, where the search would not be entirely abandoned, but will try assigning new alternative values to one (generally the last assigned) variable. The `weak-commitment' refers to this feature of the search technique, wherein it is not `strongly committed' to the current branch in the search-space as in conventional backtracking search. The aim is that the search would not be `stuck' exhaustively exploring a particular region of a search-space that might not lead to a solution. The search is instead guided by the `nogood' constraints, which are added (`learned') after each step.
The min-conflict heuristic of Minton et al. is the heuristic used to label variables. In this heuristics, a `probe' is performed when assigning a value to a variable, in which all the values in the remaining domain of the variable (i.e. values which causes no constraint violations with existing assigned variables) are considered. The value chosen is the value that causes the minimum conflict (constraint violation) with the tentative values of the still unlabelled variables.
:- compile(wcs).The WCS is invoked by calling the
wcs/2
predicate:
wcs(+Vars, ++Initial)
where Vars
is a list of variables that are to be labelled by the
search, and Initial
is a list of the initial tentative values that
are assigned to the variables. Before calling the procedure, the user must
already have set up the initial constraints so that the search can
proceed. During the search process, additional nogood constraints would be
added to direct the search.
Two example usage of the search are given: 1) a search for potentially all the solutions to the N-Queens problem, and 2) a search for the first solution to the 3SAT problems, when given the constraints on the variables in the form of Prolog facts.
The 3SAT example is simpler in that it involves only nogood constraints:
the initial constraints on the variables are simply translated into nogood
constraints before wcs/2
is called. In the N-Queens example,
additional constraints on the placement of the queens have to be specified
before wcs/2
is called.
The constraints that are specified for the WCS have to apply to both the
tentative and actual values of the variables. Tentative values are
implemented in this predicate using the repair library, and thus the
constraints have to be made known to the repair library. This is done using
the r_prop
annotation provided by the library. With this annotation, the
repair library would apply the constraint to the tentative values as well
as to the normal values.
The WCS implementation presented here is a simple and straight-forward implementation in ECLiPSe of the basic algorithm presented by Yokoo. The finite domains and the repair libraries were used. The finite domain library was used to allow for the probing step, where all valid values for a variable are tried. The repair library was used to allow for tentative values to be associated with variables, as specified in the algorithm.
The repair library is used in the following way:
r_prop
annotation.
tent_set/2
.
The predicate also has to implement the probing and restart steps of the
WCS which replace the usual tree search strategy. The probing
step tries out all the possible valid values for a variable, and picks the
value which leads to the least number of conflicts (constraint violations)
with the tentative values in the unlabelled variables. This is done with
minimize/2
from the finite domains library:
label(Var) :-
minimize((
indomain(Var),
conflict_constraints(Constraints),
length(Constraints, L) ), L).
The above tries out the available values of Var
, collect the
constraints violations on the tentative variables using
conflict_constraints
from the repair library, and counts the number
of such constraints using length/2
. The value with the minimum
number of constraint violation is selected as the binding to Var
by
this procedure.
The search restart itself is quite easy to implement in ECLiPSe, as the
just described labelling procedure, label/1
, does not leave behind a
choice-point. Thus, when a dead-end is reached in labelling values, a
simple failure will cause the procedure to fail back to the beginning,
i.e. before any variable is labelled. The restart is then implemented by
specifically creating a choice-point at the start of the search, in the
do_search/2
predicate:
do_search(Vars, _) :-
try_one_step(Vars, Vars),
% remember solution as a nogood so it would not be tried again
remember_nogood(Vars).
do_search(Vars, N) :-
% hit dead-end and failed, try again from start after recording nogoods
add_nogood(Vars), % put in most recent nogood
getval(nlabels, NL),
printf("Restart %w - labelled %w%n",[N,NL]),
N1 is N + 1,
do_search(Vars, N1).
try_one_step/2
tries out one search, with the first argument
containing the variables remaining to be labelled (initially all the
variables), and the second argument being all the variables. This would
fail if the labelling hits a dead-end and fails. In this case, the second
clause of do_search/2
will be tried, in which a new search is
started. The only difference is that a new nogood constraint will be
remembered. Note that if try_one_step
succeeds, then a solution will
have been generated. To allow for the search of more solutions, this
solution is remembered as a nogood in the first clause of
do_search/2
.
The main difficulty with implementing restart is to remember the values of
labelled variables so that it can be added as a nogood. The addition of the
nogood must be done after the failure and backtracking from the
dead-end, so that it will not be removed by the backtracking. The problem is
that the backtracking process will also remove the bindings on the labelled
variables. Thus, some means is required to remember the nogood values from
the point just before the failure, which can then be retrieved after the
failure to produce a new nogood constraint. Not only do the values
themselves have to be remembered, but which variable a particular value is
associated with has also to be remembered. This is done using the
non-logical variable feature of ECLiPSe, which allows copies of
terms to be stored across backtracking. A non-logical variable is
declared by a variable/1
declaration:
:- local variable(varbindings).
which associates the name varbindings
with a non-logical value. The
value of this variable can then be set via setval/2
and accessed
via getval/2
built-ins. In order to remember
which variable is associated with which value, all the variables being
labelled, which is organised as a list,
are copied using setval/2
2.1:
remember_nogood(Vars) :-
copy_term(Vars, NVars, _),
setval(varbindings,NVars).
To remember the current labellings when a dead-end is reached, so that a
new nogood constraint can be added for the restarted search,
remember_nogood/1
is called before the actual failure is allow to occur:
label_next(Cons, Left0, Vars) :-
pick_var(Cons, Left0, Var, Left1),
incval(nlabels),
( label(Var) ->
try_one_step(Left1, Vars)
;
remember_nogood(Vars),
fail
).
The routine first picks an unlabelled variable to label next, and
if it is successful, the
routine recursively tries to label the remaining unlabelled variables. If
not, label(Var)
fails, and the else case of the if-then-else is
called to remember the nogoods before failing.
As already described, a new nogood constraint is added by the
add_nogood/1
predicate, as shown below:
add_nogood(NewConfig) :-
getval(varbindings, Partial),
(foreach(P, Partial), foreach(V,NewConfig),
fromto(NoGoods,NG0, NG1, []), fromto(NGVars,NGV0,NGV1,[]) do
(nonvar(P) ->
V tent_set P,
NG0 = [P|NG1],
NGV0 = [V|NGV1]
; NG0 = NG1, % keep old tentative value
NGV0 = NGV1
)
),
NoGoods ~= NGVars r_prop. % no good
If a variable had been labelled in the previous search, the labelled value becomes the tentative value. Otherwise, the variable retains the original tentative value.
The nogood constraints are implemented via the built-in sound difference
operator, ~=/2
. For example,
[A,B,C] ~= [1,2,3]
states that the variables A
, B
and C
cannot take on the values of 1
, 2
and 3
respectively
at the same time. The operator will fail when [A,B,C]
becomes ground
and take on the values [1,2,3]
. If any of the variables take on a
value different from what is specified, ~=/2
will (eventually)
succeed. The operator thus acts passively, waiting for the variables to be
instantiated and then check if they are taking on the `nogood' values, and
does not propagate or deduce any further information.
The algorithm described by Yokoo does not specify how the next variable is
selected for labelling. In this routine, it is done by the
pick_var/4
predicate:
pick_var(Cons, Left0, Var, Left) :-
term_variables(Cons, Vars0),
deleteffc(Var0, Vars0, Vars1),
(is_validvar(Var0, Left0, Left) ->
Var = Var0 ; pick_var1(Vars1, Left0, Var, Left)
).
The next variable to be labelled is chosen from the set of variables whose
tentative values are causing conflict. The repair library maintains the
(repair) constraints which are causing conflict, and any variable which are
causing conflict will occur in these constraints. The set of conflicting
repair constraints is passed to pick_var/4
in the first argument:
Cons
. term_variables
is used to obtain all the variables that
occur in these constraints. The fd predicate deleteffc
is then used
to select a variable (picking the one with the smallest domain and most
constraints), and then this variable is checked to make sure that it is
valid variable to be labelled, i.e. that it is one of the variables to be
labelled. The reason for this check is that it is expected that the WCS
routine will be used as part of a larger program, and the program may use
the repair library itself, and thus Cons
may contain constraints
unrelated to the WCS labelling.
As already stated, the built-in ~=/2
used for nogood constraints is
passive. More powerful propagation can be added to the nogood constraint if
the constraint is defined by the user. To try this out, a somewhat more
powerful constraint was tried out. This constraint does forward checking,
in that when only one variable specified in a nogood remains unlabelled,
and the labelled variables are labelled to the values specified by the
nogood, the constraint that this last variable cannot take on its nogood
value can be propagated. This increase the efficiency of the search in many
cases, although at the price of a slightly more complex implementation
of the nogood constraint.
The nogood constraint is implemented as shown:
nogood([X|Xs], [V|Vs]) :-The nogood-constraint is set up by
( X==V -> nogood(Xs, Vs)
; var(X) -> nogood(Xs, Vs, X, V)
; true
).
nogood([], [], X1, V1) :- X1 ## V1.
nogood([X|Xs], [V|Vs], X1, V1) :-
( X==V -> nogood(Xs, Vs, X1, V1)
; var(X) -> suspend(nogood([X1,X|Xs], [V1,V|Vs]), 3, X-X1->inst)
; true
).
nogood(NGVars, NoGoods) r_prop.The implementation checks whether NGVars matches NoGoods and causes failure if this is the case. If a non-matching variable-value pair is encountered, the constraint disappears. If a variable is encountered, nogood/4 continues checking, and if the variable turns out to be the only one, the corresponding values gets removed from its domain. If a second variable is encountered, the constraint re-suspends until at least one of them gets instantiated.
Obviously, this is still a relatively naive implementation of the nogood-technique. As the number of nogoods grows, implementing them via indivudual constraints will become more and more infeasible, and optimisation techniques like merging and indexing of the nogoods will be needed.