This notes takes you step by step on how to create LISP programs to solve depth-first search problems.
It is particularly easy to write a program for depth-first search. There are lots of such programs avaialble on the WWW. Doubly recursive procedures penetrate a tree in a very efficient manner. However, it is not easy to modify these codes and adapt them for breadth first and other searches.
Let us work on a queue-oriented search. Our queue will consist of partial paths. Using queues of partial paths is hard at first, but once we have done depth first search, it is very easy to modify these codes to do other searches.
SEARCH simply converts its first argument into a one-element queue for
the benefit of SEARCH1. Then SEARCH1 examines the queue, testing the first
path in the queue for success.
If the last node in the first path is not the finish node, then SEARCH1
extends the path, modifies the queue, and hands the modified queue to another
copy of SEARCH1.
(DEFUN SEARCH (START
FINISH)
(SEARCH1 (LIST START)
FINISH)
; initialize
(DEFUN SEARCH1(QUEUE
FINISH)
(COND ( ( NULL
QUEUE) NIL)
; return NIL if queue is empty
( ( EQUAL FINISH
( ( CAR QUEUE) T)
;return T if goal is found
( T (SEARCH1
<appropriate merge of
(EXPAND (CAR QUEUE) ) and QUEUE>
FINISH) ) ) )
Here, EXPAND returns the children of a node, given that node an as argument.
Before we write expand, let us look at representing the data in a program.
If we are dealing with trees only, nested lists would do nicely. If we want to handle nets also, it is better to use symbols and properties.
Nodes and their children can be represented by symbols and the arcs can be represented by properties.
For example,
(SETF (GET 'S CHILDREN) '(L O) )
captures the fact that S is a parent whose children are L and O.
(SETF (GET 'L CHILDREN) '(M F) )
captures the fact that L is a parent whose children are M and F.
By repeating the above command types, we can describe an entire tree.
For example, the tree I have in mind is described below:
(SETF (GET 'S
CHILDREN) '(L O) )
(SETF (GET 'L
CHILDREN) '(M F) )
(SETF (GET 'M
CHILDREN) '(N) )
(SETF (GET 'N
CHILDREN) '(F) )
(SETF (GET 'O
CHILDREN) '(P Q) )
(SETF (GET 'P
CHILDREN) '(F) )
(SETF (GET 'Q
CHILDREN) '(F) )
It is to your benefit to draw a picture of this tree and keep it in front of you as you read the rest of this page.
Having defined how the nodes are connected, we are now ready to write the code for EXPAND.
(DEFUN EXPAND (NODE) (GET NODE 'CHILDREN) )
The method of merging the new children into the old QUEUE depends upon the search strategy. For a simple depth-first search, the appropriate form is
(APPEND (EXPAND (CAR QUEUE) (CDR QUEUE) )
So a simple-minded depth-first search looks like this
(DEFUN DEPTH (START
FINISH)
; Note change in name to DEPTH
(DEPTH1 (LIST START)
FINISH)
; initialize
(DEFUN DEPTH1(QUEUE
FINISH)
; Note change in name to DEPTH1
(COND ( ( NULL
QUEUE) NIL)
; return NIL if queue is empty
( ( EQUAL FINISH
( ( CAR QUEUE) T)
;return T if goal is found
( T (DEPTH1
; try again with new queue
(APPEND
(EXPAND (CAR QUEUE)
; new node at head
(CDR QUEUE) )
; Rest of queue
FINISH) ) ) )
This program does the job, but very little. It simply returns T or NIL.
A. It would be nice if we get to know the nodes along the path that led us to the goal.
B. This program cannot handle nets because there is no check to prevent it from going into endless loops.
How to get the program to return a PATH?
We have to pack more information into the elements of the data structure
QUEUE. Until now, the elements represented the nodes remaining to be tested.
So the QUEUE would have looked like this:
(S)
(L O)
(M F O)
(N F O)
(F F O)
Instead, we want the elements to represent paths, rather than just nodes. Each path starts with the starting node and extends up to the node whose children are not yet explored. Then the QUEUE develops like this
( (S) )
( (L S) (O S) )
( (M L S) (F L S) (O
S) )
( (N M L S) (F L S) (O
S) )
( (F N M L S) (F L S) (O
S) )
Because of the new format in QUEUE we need to change DEPTH. First, FINISH is compared with (CAAR QUEUE) rather than with (CAR QUEUE). Second, instead of returning T, the path on which T is located is returned. Finally, a small adjustment is made to present the result in reverse order which is the natural order, namely from source to goal.
(DEFUN DEPTH (START
FINISH)
; Note change in name to DEPTH
(DEPTH1 LIST (LIST START)
FINISH) ; Note slight change
(DEFUN DEPTH1 (QUEUE
FINISH)
; Note change in name to DEPTH1
(COND ( ( NULL
QUEUE) NIL)
;return NIL if queue is empty
( ( EQUAL FINISH
( ( CAAR QUEUE) )
;Note small change to CAAR
( REVERSE ( CAR QUEUE)
) )
;Note small change to CAAR
( T (DEPTH1
; try again with new queue
(APPEND
(EXPAND (CAR QUEUE)
; new node at head
(CDR QUEUE) )
; Rest of queue
FINISH) ) ) )
Of course we have to change EXPAND too. Rather than taking a node and
returning a list of its children, it should take a path, find the children
of the node at the end of the path and return a list of new paths. Each
new path will consist of the original path with one of the children tacked
on.
This can be arranged as follows:
(DEFUN EXPAND (PATH)
; initial version
(MAPCAR # '(LAMBDA (CHILD) (CONS
CHILD PATH) )
(GET (CAR
PATH) 'CHILDREN) ) )
The MAPCAR arranges for a new path to be constructed each child found just beyond the end of the old path.
Still this does not work if there are loops.
If we wish to handle nets, as well as trees, then we have to examine the paths offered up by the EXPAND operation, check to see if it has a new node that is already present elsewhere in the path and purge that.
(DEFUN EXPAND (PATH)
; IMPROVED version
(REMOVE-IF
# '(LAMBDA (PATH)
(MEMBER (CAR PATH)
(CDR PATH) ) ) ; flush circular paths
(MAPCAR # '(LAMBDA (CHILD) (CONS
CHILD PATH) )
(GET (CAR
PATH) 'CHILDREN) ) ) )
If we wish to use this on a net, rather than a tree, the data capture
operations done earlier with SETF expressions should be done with reference
to a net.
Department of Computer Science
University of California at Davis
Davis, CA 95616-8562