One way to limit the scope of backtrack search is to keep a record of the number of backtracks, and curtail the search when this limit is exceeded.
You can find an implementation of Bounded Backtrack Search (BBS)
in the file lds.ecl in the doc/examples directory of your
ECLiPSe installation.
The predicate defining bounded backtrack search is
bounded_backtrack_search
, and it takes two arguments, a list of
variables and an integer (the limit on the number of backtracks).
An example invocation is:
?- [X,Y,Z]::1..3, X+Y+Z#=6, bounded_backtrack_search([X,Y,Z],4).The answers are returned on backtracking in the following order:
Backtrack limit exceeded
.
The implementation uses several facilities of ECLiPSe, including non-logical variables and catch and throw:
:- local variable(backtracks), variable(deep_fail).
bounded_backtrack_search(List,Limit) :-
setval(backtracks,Limit),
block(bbs_label(List),
exceed_limit,
(writeln('Backtrack limit exceeded'), fail)
).
bbs_label([]).
bbs_label([Var|Vars]) :-
limit_backtracks,
indomain(Var),
bbs_label(Vars).
limit_backtracks :-
setval(deep_fail,false).
limit_backtracks :-
getval(deep_fail,false), % may fail
setval(deep_fail,true),
decval(backtracks),
(getval(backtracks,0) -> exit_block(exceed_limit) ; fail).
Credit search is a tree search method where the number of nondeterministic choices is limited a priori. This is achieved by starting the search at the tree root with a certain integral amount of credit. This credit is split between the child nodes, their credit between their child nodes, and so on. A single unit of credit cannot be split any further: subtrees provided with only a single credit unit are not allowed any nondeterministics choices, only one path though these subtrees can be explored, i.e. only one leaf in the subtree can be visited. Subtrees for which no credit is left are pruned, i.e. not visited.
The following code (a replacement for labeling/1) implements credit search. For ease of understanding, it is limited to boolean variables:
% Credit search (for boolean variables only)Note that the leftmost alternative (here X=0) gets slightly more credit than the rightmost one (here X=1) by rounding the child node's credit up rather than down. This is especially relevant when the leftover credit is down to 1: from then on, only the leftmost alternatives will be taken until a leaf of the search tree is reached. The leftmost alternative should therefore be the one favoured by the search heuristics.
credit_search(Credit, Xs) :-
(
foreach(X, Xs),
fromto(Credit, ParentCredit, ChildCredit, _)
do
( var(X) ->
ParentCredit > 0, % possibly cut-off search here
( % Choice
X = 0, ChildCredit is (ParentCredit+1)//2
;
X = 1, ChildCredit is ParentCredit//2
)
;
ChildCredit = ParentCredit
)
).
What is a reasonable amount of credit to give to a search? In an unconstrained search tree, the credit is equivalent to the number of leaf nodes that will be reached. The number of leaf nodes grows exponentially with the number of labelled variables, while tractable computations should have polynomial runtimes. A good rule of thumb could therefore be to use as credit the number of variables squared or cubed, thus enforcing polynomial runtime.
Note that this method in its pure form allow choices only close to the root of the search tree and disallows choices completely below a certain tree depth. This is too restrictive when the value selection strategy is not good enough. A possible remedy is to combine credit search with bounded backtrack search.
Another form of incomplete tree search is simply to use time-outs. The branch-and-bound primitives min_max/6,8 and minimize/6,8 allow to specify a maximal runtime. If a timeout occurs, the best solution found so far is returned instead of the proven optimum.
A general timeout can be implemented as follows. When Goal has run for more than Seconds seconds, it is aborted and TimeOutGoal is called instead.
:- set_event_handler(timeout, exit_block/1).
timeout(Goal, Seconds, TimeOutGoal) :-
block(
timeout_once(Goal, Seconds),
timeout,
call(TimeOutGoal)
).
timeout_once(Goal, Seconds) :-
event_after(timeout, Seconds),
( call(Goal) ->
cancel_after_event(timeout)
;
cancel_after_event(timeout),
fail
).
Limited discrepancy search (LDS) is a search method that assumes the user has a good heuristic for directing the search. A perfect heuristic would, of course, not require any search. However most heuristics are occasionally misleading. Limited Discrepancy Search follows the heuristic on almost every decision. The ``discrepancy'' is a measure of the degree to which it fails to follow the heuristic. LDS starts searching with a discrepancy of 0 (which means it follows the heuristic exactly). Each time LDS fails to find a solution with a given discrepancy, the discrepancy is increased and search restarts. In theory the search is complete, as eventually the discrepancy will become large enough to admit a solution, or cover the whole search space. In practice, however, it is only beneficial to apply LDS with small discrepancies. Subsequently, if no solution is found, other search methods should be tried.
The definitive reference to LDS is:
Limited Discrepancy Search, Harvey and Ginsberg, pp.607-613, Proc. IJCAI'95
This reference also suggests that combining LDS with Bounded Backtrack Search (BBS) yields good behaviour. Accordingly the ECLiPSe LDS module also supports BBS and its combination with LDS.
We start by assuming a static heuristic, which is a complete assignment to the problem variables specified in advance of the search. The predicate supporting static LDS takes a list of variables (those which are to be labelled) and a list of values (one heuristic value for each variable, respectively). Each variable has a finite domain, and its heuristic value should belong to its domain (though the LDS search can still succeed even if this is not the case).
The measure of discrepancy, in this case, is simply the number of variables labelled differently to the heuristic. Thus the maximum discrepancy is just the number of variables to be labelled.
LDS search is implemented in the file lds.ecl in the doc/examples directory of your ECLiPSe installation. You can copy this file and load it with
:- use_module(lds).Static LDS search is then available via the predicate static_lds(Var, Vals, Discrepancy) whose arguments are
?- [X,Y,Z]::1..3, X+Y+Z#=5, static_lds([X,Y,Z],[1,2,3],D).The answers are returned on backtracking in the following order:
Often the heuristic value is calculated on the fly, during search. To cope with this we use the ECLiPSe ``tentative value'' facility in ECLiPSe's repair library. The heuristic is stored with the variable as its tentative value.
The tentative value may be changed during search. For example if a variable is instantiated as a consequence of constraint propagation during search, its tentative value is automatically changed to its actual value.
Dynamic LDS search is available in ECLiPSe via the predicate dynamic_lds(Vars, Discrepancy). Each variable in the list of variables Vars must have a tentative value.
An example invocation is:
?- [X,Y,Z]::1..3, [X,Y,Z] tent_set [1,2,3], X+Y+Z#=5,The answers are returned on backtracking in the following order. Notice that the first solution has a discrepancy of 0, because constraint propagation instantiates the third variable to 2, thus changing its tentative value from 3 to 2.
dynamic_lds([X,Y,Z],D).
The two search techniques, BBS and LDS, can be merged quite simply in ECLiPSe, so that for each discrepancy level only a limited number of backtracks are allowed.
An example invocation is:
?- Vars=[X,Y,Z], Vars::1..3, Vars tent_set [1,2,3], X+Y+Z#=6,The answers are returned on backtracking in the following order:
bbs_dynamic_lds(Vars,4,D).