CS 2360 Winter 95 (lab 05v.04)

lyman@cc.gatech.edu


Announcements

You should also receive a separate handout with a pictorial representation of the example trees used by this lab.

Note although the text of this lab will appear on the Web soon.... the pictures may not appear till later.


Today's Topics

The topics to be covered today are:

This lab's objective is to give you some practical experience with Depth and Breadth First search and with the generic process of searching in general. To simplify matters the searching is done using a tree. This is done since the tree is a special kind of graph with properties that makes the code slightly simpler.

Prerequisites

Today's Lab requires that you have completed the previous labs. Additionally, it assumes that you have seen and at least partially absorbed the material up to from Lecture 9. It also assumes that you have:

Step 1. loaded (or evaluated) the init.lisp file that you downloaded in Lab 1.

Step 2. You should download the file ~gt7510f/2360/lab5.lisp at this time, also. After opening it in a Fred Buffer, you should evaluate this file.


Trees Revisited

We will be searching through trees in this lab. Trees are special kinds of graphs. Namely, they have the property that there are no cycles. In other words there is one and only one path from any node to any other node. If one tree covers all the nodes in a graph then if you try to add one more edge then you would create a cycle.

In lab 4 we played a bit with trees with 2 children and then with trees with 4 children. In this lab we'll be using the "n-ary" Tree ADT. Meaning that a node can have as many or as few children as is needed. [NOTE: there was not restrictions on how many connections a node in a graph could have. Therefore our "special case" graph must also meet these conditions.]

The following is a example of a "n-ary" tree.

Figure 1

So our tree abstraction has the following specification. A tree is either empty or it consist of a tree node that

For our purposes we will represent the links by the chidren nodes themselves. Just like for the trees in the previous lab. Only this time there can be n children.

The implementation looks like the following:.

                   The empty tree is represent by nil.
                   A tree node is of the following format
                             (  id  tree-node*  )
                   where there can be 0 or more tree-nodes ( that is what the '*' means)
Trees have the following operations defined for them.

                   tree-make
                   tree-empty
                   tree-id
                   tree-children
                   tree-dfs
                   tree-bfs


Depth First Search

Depth First Search (DFS) exhaustively searches through successive "successors" until a solution is found. If no solution is found then it will try alternative successor to the current node. If no successors exists then the search stops.

The should look familiar to folks. The function tree-search from lab4 was doing depth first search. The depth first search code that appears here is just generalized to handle trees with n children.

The nodes in the following tree are labeled in the order that DFS would visit them.

Figure 2.

Additionally, the search function has been instrumented to optionally print out information along the course of the search. Ignore the code wrapped up inside the when and the :print argument if you just want to understand the searching aspects of the Lisp code.

There are three example trees, *tree1* ,*tree2* , and *tree3*, declared within the Lisp code. Try searching through them. For example:

                   (tree-dfs  'worf *tree1* )
                 or 
                   (tree-dfs  'worf *tree1* :print t )
The second form will print out information along the course of the search.

Additionally, try to trace tree-dfs. Notice the size of the arguments passed to each recursive call. Do they successively get smaller or larger? We'll be comparing that to what appends with Breadth First search.


Breadth First Search

Breath First Search (BFS) looks at all the successors of a node and if no solution is found searches their successors. Once we run out of candidates the search stops.

To do BFS we need to use a queue. Take the following tree

Figure 3

At node a the list of nodes to be examined later, if necessary, would be.

( b c d )

If a is not a solution then the first node on the queue should be examined. In this case, is b. So we'll pop b off the queue. So after b has been considered and rejected, the list should look like.

(c d e )

Where e is appended onto the end of the queue. This continues until the queue is empty or the item being search for is found. A Queue ADT is also contained within the file. It is based upon lists and largely consists of wrapping some new names around some familiar functions.

NOTE: the lexicographic order of the above nodes is the order in which BFS will visit them.

The BFS code included in lab5.lisp also contains extra "stuff" so that additional information may be printed out while the code is searching.

You should repeat the searches you made. For example:

                   (tree-bfs  'worf *tree1*  )
                 or 
                   (tree-bfs  'worf  *tree1*   :print t )

Also trace tree-bfs-hlp on some searches. Do the arguments get successively larger or smaller with each call? Compare this with DFS.

Compare the number of steps needed by each algorithm to complete it search task for a given target.


Abstraction of Processes

So far in this class we have talked about the abstract of datatypes on several occasions. Notice however that in Lisp functions are a first class datatype. So we should be able to abstract those too!

A first class datatype means that you can construct and use the datatype wherever you want. You can use it as an argument to a function. Or as the result of a function.

But what is a function? A Lisp integer represents a mathematical construct. You can say it describes that construct. A Lisp string represents a string. A function describes a process.

Notice that BFS and DFS mostly do the same thing. In fact, we could reconstruct DFS so that it too used a queue. However, instead of pushing successors onto the end of the queue it pushes successors onto the front of the queue. This will guarantee that the first successor's "children" are examined before the other successors, if any, are considered.

Think of how you would rewrite DFS to do this. For example, using the tree from the DFS section. The queue would go through the following stages.

                             ( 1 )      
                             ( 2 4 5 )  
                             ( 3 4 5 )
                             ( 4 5 )
                             ( 5 )
                             ( 6 7 )
                             ( 7 )
                             (   )
       


Generic Search

There is a generic search function in lab5.lisp. This function can be used to preform any type of search that involves using a queue. It abstracts away the process of how successors are generated. Additionally, it abstracts away the process of how these new successors are to be combined with the current queue.

A generic description of a queue utilizing search process is:

                   If the queue is empty then return NIL ( search failed )
                   If when the element on the head of the queue is passed to goal-p
                             returns true then solution has been found.
                   Otherwise
                             Update the remainder of the queue with the head's 
                             successors and call recursively.
Your job is to use this generic search to implement both DFS and BFS by passing in the correct arguments. For example

                   (generic-search  A (queue-make *tree1* )    B  C )     ; do DFS
                   (generic-search  D (queue-make *tree1* )    E F  )     ; do BFS
where A,B,C, D, E, and F are the appropriate functions. You might need to use a lambda function or two. For example #'(lambda (x) (eql 'worf (tree-id x )) ) will return true if "root" the argument passed in is eql to worf.