ECS 170: Introduction to AI
Programming Steps for Searching
Prof. Rao Vemuri

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


Page last modified on 12/12/2002