The term Nondeterministic algorithm refers to a class of algorithms in which computational progress may correctly follow several different paths. Since we are satisfied that the algorithm computes the solution to a particular problem even if only one such path leads to termination with a solution, the implementation of nondeterministic algorithms requires a strategy for exploring the multiple paths that may emanate from some choice point. The simplest such strategy is called backtracking, and involves having control return to earlier choice points when reaching a dead end.
Sequential nondeterministic programs (i.e. programs executing on a sequential machine that implement nondeterministic algorithms) have a distinctive continuation structure, since they must be able to run both forwards and backwards (forwards when successfully extending a partial solution, and backwards when failing to do so). Consequently, such programs written using CPS must pass continuations, a success continuation, k, to be used in the former case, and a failure continuation, q, to be used in the latter.
Starting from some initial value, a backtrack program incrementally constructs a solution by extending the current partial solution with some legal choice. If the partial solution becomes a complete solution it is returned. Otherwise, upon reaching a dead end, the program backtracks to the previous choice point, discards the choice it made there, and continues as though that choice were illegal in the first place. It may happen that every possible choice is examined and rejected, in which case the problem has no solution.
In Scheme we can write a general backtracker, parameterized by the specifics of the problem being solved:
These nine parameters define the problem space as follows:(define solve (lambda (*initial* *complete?* *extensions* *first* *rest* *empty?* *extend* *legal?* *top-k* *do* *undo*)
(*initial*) returns an initial partial solution (psol) (*complete?*psol) true if partial solution psol is a complete solution (*extensions* psol) creates the set of all possible choices to extend psol (*first* choices) selects next choice (*rest* choices) returns remaining choices (*empty?* choices) true if all choices are exhausted (*extend* psol choice) extends partial solution by given choice (*legal?* psol) true if psol is a legal partial solution (*top-k* sol q) top-level success continuation (*do* psol) performs any required state change when recording new choice (e.g. for graphics) (*undo* psol) retracts any required state change when backtracking
Here is the code for solve. Look specifically at how the two continuations (k and q) are used.
(define solve (lambda (*initial* *complete?* *extensions* *first* *rest* *empty?* *extend* *legal?* *top-k* *do* *undo*) (let try ([psol (*initial*)] [choices (*extensions* (*initial*))] [k *top-k*] [q (lambda () 'failed)]) (if (*complete?* psol) (k psol q) (if (*empty?* choices) (q) (let* ([new-psol (*extend* psol (*first* choices))] [new-k (lambda (new-psol new-q) (*do* new-psol) (try new-psol (*extensions* new-psol) k new-q))] [new-q (lambda () (*undo* new-psol) (try psol (*rest* choices) k q))]) (if (*legal?* new-psol) (new-k new-psol new-q) (new-q))))))))
The success continuation k is used as in CPS to carry the computation forward and return the answer. The failure continuation q is invoked to backtrack when the choice list becomes empty. This is why a new q is constructed in the call (try new-psol ...), in which we have extended the current partial solution and are moving forward to extend the next with a recursive call. The new failure continuation backtracks with a call to try with the current partial solution, starting from the next choice.
The files knight.ss, eightqueens.ss and goodseq.ss contain example programs that require solve.ss. The first 2 are graphical and also require several graphics modules (built out of the OOP material from Lab 8, in fact).
Now consider the good sequences problem. A good sequence is one that contains no adjacent repeated subsequence, for example (3 2 1 3 2) but not (3 2 1 2 1 3) (because of the 2 1 2 1). The top level continuation prints the solution and invokes the failure continuation, so the that running (goodseq n) prints all good sequences of length n. Try it with different values of n, such as 4 8 and 12.
This last problem is optional. You are encouraged to do it, but don't hand it in.