CS 4600
Introduction to Intelligent Systems
Project #1
Introduction to LISP

Numbers

Due: September 2, 2003 23:59:59 EST
Please email your code to Steve Marlowe with the subject line "cs4600 project #1". Also, be certain to include your name in your message.

The assignment is worth 2% of your final grade.

Why?

The purpose of this project is to give you an excuse to learn how to hack in LISP. You will be asked to write a few simple LISP functions, mainly to manipulate lists. Eventually, you will solve a simple graph search problem. As noted above, this is worth 2% of your final grade, but its real value is that it will help you to become comfortable enough with LISP that you will find it much easier to do the next assignment (though I'd like to point out that 2% is still 2%, and even those of you who dream in defmacro might be happy that you earned that 2% come the end of the semester).

References

First off, the Class Resources pages should point you to many online and offline references. Locally, you should be able to run lisp images on any CNS-supported machine. Here's what I think we know from CNS: "We have Allegro CL from Franz available on our Unix and Windows environments. The default version is 6.2. The images are called alisp, alisp8, mlisp and mlisp8. The distribution directory is /usr/local/allegrocl."


Each of the functions below should be put in a file/attachment named yourgtaccount.lisp.

Basic LISP

  1. Write my-member, which determines if a given element is in a list.
    (my-member 'a '(b c j a)) => (A);;;; Problem 1
    (my-member '(c d) '(a b c (a b) (c d) j k)) => ((C D) J K)
    (my-member 'a '(b c (a d) e)) => nil
          
  2. Write my-insert, which inserts a given element into a list at a given location (zero based). If the location is too small (i.e. negative) or too large (i.e. greater than or equal to the length of the list), the function should place the new element at the beginning or end of the list as appropriate.
    (my-insert 'a '(b c d e) 0) => (A B C D E)
    (my-insert 'a '(b c d e) 3) => (B C D A E)
    (my-insert 'a '(b c d e) 14) => (B C D E A)
          
  3. Write my-remove-dups, which removes duplicate entries in a list.
    (my-remove-dups '(a b a c a d)) => (B C A D)
    (my-remove-dups '(a b (a c) a d)) => (B (A C) A D)
    (my-remove-dups '(a b (b c) a (b c) d)) => (B A (B C) D)
          
  4. Write my-intersection, which returns the intersection of two lists.
    (my-intersection '(a b c) '(c a e)) => (A C)
    (my-intersection '(a b (a c) c) '(a c b)) => (A B C)
    (my-intersection '(a b (a c) c) '((a c) b)) => (B (A C))
          
  5. Write dyadic-apply, which takes a function and two lists and returns a list of the result of the function applied to pair-wise elements from the two lists. If the lists are of unequal length, processing stops when the end of the shorter list is reached.
    (dyadic-apply #'+ '(1 2 3) '(6 5 4)) => (7 7 7)
    (dyadic-apply #'list '(a b c) '(1 2 3 4)) => ((A 1) (B 2) (C 3))
          
  6. Write tree-sum, which returns the sum of all numbers in a list, including those numbers in lists within the list, ad infenitum. Note that there may be elements in the input that are not numbers.
    (tree-sum '(1 2 3 4 5)) => 15
    (tree-sum '(a 1 b 2)) => 3
    (tree-sum '(a (1 (2 (3 b) c) d e) f 4)) => 10
          
  7. Write my-flatten, which "flattens" a list. This means that the function returns a list with no nested lists such that the elements are the same and in the same order as they were in the printed version of the original list.
    (my-flatten '(a b (c d))) => (A B C D)
    (my-flatten '(a (b (c (d (e f))) g (h i (j k)) l (m)) (((n))))) =>
    	    (A B C D E F G H I J K L M N)
          
  8. Write full-reverse, which reverses every symbol and every nested list in its input.
    (full-reverse '(a b c d)) => (D C B A)
    (full-reverse '(a (b c) d)) => (D (C B) A)
    (full-reverse '((a b) (c (d e) f g (h i) j k) l m)) =>
    	      (M L (K J (I H) G F (E D) C) (B A))
          
  9. Write my-subst, which replaces all occurences of a given element with another. Your function should replace lists as well as atoms and correctly replace arbitrarily nested occurences.
    ;; replace all occurrences of B with A
    (my-subst 'a 'b '(t h b d s t b r n e r)) => (T H A D S T A R N E R)
    (my-subst 'a 'b '(a b (a b (a b)) (a b))) => (A A (A A (A A)) (A A))
    
    ;; replace (P Q) with A
    (my-subst 'a '(p q) '(p q (p q) (((p q) a b (p q a))))) =>
    	  (P Q A ((A A B (P Q A))))
          
Intermediate LISP
  1. In many search problems, the state space will be represented a priori by a graph. This graph can be stored in a variety of equivalent data structures including an adjacency matrix, an adjacency list, or as a list of edges. In this problem, write a function to convert an adjacency list to a list of edges.

    An adjacency list might be stored in the following way. For each node, a list is kept of all nodes that are one step away. This list has the form (source node . list of adjacent nodes). For example, if you can reach nodes B, C, and D from node A, the corresponding list would be (A B C D). By grouping all of these lists into a single list, the graph is fully represented.

    A list of edges is a less compact way to represent a graph, but may provide easier access if the edges have weights. For example, we could represent an edge from node A to node B as (A B). If this edge had a weight of 7, the representation might change to (A B 7).

    Note that in both representation, we must specify whether edges are uni- or bi-directional. That is, does (A B) imply (B A) or must (B A) be explicit in the representation if the edge in that direction exists? For this problem, assume that all edges are unidirectional meaning that if B is adjaceny to A (i.e. (A B)), A is not necessarily adjacent to B (i.e. (B A) is *not* implied).

    ;; define a graph via an adjacency list
    (defparameter adjlist '((a b c) (b e) (c d) (d b c e)
    		      (e d f h) (f e) (g h) (h e g)))
    
    (adj-list-to-edge-list '((a b))) => ((A B))
    (adj-list-to-edge-list '((a b c d))) => ((A B) (A C) (A D))
    (adj-list-to-edge-list adjlist) => ((A B) (A C) (B E) (C D) (D B) (D C)
    				   (D E) (E D) (E F) (E H) (F E) (G H)
    				   (H E) (H G))
          
    It is important to realize that the order of the nested lists (i.e. the edges) is not important. So '((A B) (A C) (B C)) is equivalent to '((B C) (A C) (A B)) for example.

  2. Many search problems determine and return a path. A simple representation of a path is a list of nodes along the way. Thus, a path from node A to node B to node C would be represented as (A B C). It is generally somewhat difficult to store and retrieve the desired path while you are performing a search. Although you could pass the "path so far" to each call of a recursive search and then just return that path when the goal is reached, a less memory intensive solution is desirable.

    One common technique that uses much less memory is to simply store the optimal predecessor of each node. Since a search will define a tree rooted at the source node, we know that each node was reached from exactly one other node. If we simply store this so-called "parent node," we can easily reconstruct the desired path given a goal node.

    For example, if our search was looking for a path to node E from node A and it determined that we got to B from A, to C from A, to D from B, and to E from D, we might have a data structure that looks like this:

    ((B A) (C A) (D B) (E D))
        
    Each internal list holds a node in the first position and its parent in the second. With this representation, it is easy to reconstruct the path. Of course, contructing the path from the start node to the goal node will require another search (note that from node A, we can get to nodes B or C), but the path is unambiguous when reconstructed backward (i.e. from goal to start node). Luckily, it is a trivial matter to reverse the path after it is reconstructed.

    Write a function that takes a list of nodes and parents and returns a path from a given start node to a given goal node. You may assume that the path will always exist and be unambiguous.

    (reconstruct-path 'a 'c '((b a) (c b))) => (A B C)
    (reconstruct-path 'a 'e '((b a) (c a) (d b) (e d))) => (A B D E)
    (reconstruct-path 'b 'e '((g a) (a c) (c d) (b a) (i b) (f e)
    			  (j i) (r i) (d r) (e g))) => (B I R D C A G E)
    
  3. Traversing adjacent nodes is a common operation to many search problems. Although it is sometimes unimportant, it often is necessary to select the next node according to some criteria. In these cases, it is useful to have a data structure that stores the possible nodes to traverse and can easily provide the next appropriate node. Although there are a number of more efficient implementations, one simple way to create such a priority queue is with a sorted list. In this problem, you are to write a function to add a weighted edge to a simple priority queue. The structure of the edge is (Node 1 Node 2 Weight). The queue should hold the edges in order by ascending weights.
    (add-edge '(a b 3) nil) => ((A B 3))
    (add-edge '(c d 5) '((a b 3))) => ((A B 3) (C D 5))
    (add-edge '(a c 1) '((a b 3) (c d 5))) => ((A C 1) (A B 3) (C D 5))
    (add-edge '(b d 4) '((a c 1) (a b 3) (c d 5))) =>
    			     ((A C 1) (A B 3) (B D 4) (C D 5))
        
Search

Write a function that takes the description of a graph, a start node, and a goal node , and determines if the goal node can be reached from the given starting point. The return value should be a path connecting the nodes (i.e. a list of nodes) or nil if no such path exists. Your path does not have to be optimal. The description of the graph will be an adjacency list represented as a list of lists. Each internal list is of the form (Node . List of adjacencies). All edges are unidirectional. Please solve this problem by implementing a depth-first search.

  ;; define a graph via an adjacency list
  (defparameter adjlist '((a b c) (b e) (c d) (d b c e)
	  	        (e d f h) (f e) (g h) (h e g)))
This means that you can get to B & C from A in one step, to E from B, to D from C, to B, C, & E from D, etc.
  (dfs adjlist 'a 'g) => (A C D E H G)
  (dfs adjlist 'e 'c) => (E D C)
  (dfs adjlist 'h 'a) => nil
Note that these solutions are not necessarily unique. For example:
  (dfs adjlist 'a 'g) => (A B E H G)
is also acceptable.