A General Backtracking Program

Chapter: Nondeterministic Programs
...Section: A General Backtracking Program

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:

(solve *initial* *complete?* *extensions* *first* 
       *rest* *empty?* *extend* *legal?* *return*)
These nine parameters define the problem space as follows:
(*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 first of our choices
(*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
(*return* sol) returns the (complete) solution sol

Here is the code for solve. Look specifically at how the two continuations are used. The success continuation is k and the failure continuation is q. Notice that k takes a parameter (what to do with a solution) and q needs no parameter.

(define solve
  (lambda (*initial* *complete?* *extensions* *first* 
		     *rest* *empty?* *extend* *legal?* *return*)
    (letrec ([try
	      (lambda (psol choices k q)
		(if (*complete?* psol) (k psol)  ; finished, so apply success k
		    (if (*empty?* choices) (q)   ; exhausted all choices, so backtrack
			(let ([new-psol (*extend* psol (*first* choices))]) 
			  (if (*legal?* new-psol)
			      (try new-psol (*extensions* new-psol) k
				   (lambda () 
				     (try psol (*rest* choices) k q)))
			      (try psol (*rest* choices) k q))))))])
      (try *initial* (*extensions* *initial*) *return*
	   (lambda () 'failed)))))1

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.

Q. 19
Note that the body of the newly constructed failure continuation is identical to the "else" branch of the conditional that tests legality. Why does this have to be so?



Exercise 10

The program for solve is correct formally, but much more complicated than it needs to be. It can be re-written without any continuations in such a way that it becomes deeply-recursive in the backward direction only. The file solve.ss contains this program together with an implementation of the good sequences problem.

Q. 20
Are you wondering why our scheme program produces 3 1 3 2 3 as the first good sequence of length 5, whereas our "backtracking by hand" above produced 3 2 3 1 3 as its first good sequence?


  1. Modify the code for solve so that it uses no continuations and uses deep-recursion only when backtracking.
  2. It is possible to look at the original code for solve and see immediately that success continuations can be eliminated without requiring the substitution of deep recursion. What are the tell-tale signs?




rhyspj@gwu.edu