Rice University

COMP 210: Principles of Computing and Programming

Lecture #33       Fall 2003

Why we use set!

We saw last time, scheme passes arguments "by value":

When calling a function, the arguments (values) are copied onto a piece of paper, and handed to the function. Even if the function re-writes those pieces of paper, the caller's data isn't modified.

On the other hand,

 

Breadth-first search

We previously saw "depth-first search" -- finding an airline path between cities by recurring on its neighbors (with a base case of starting at the destination). We found that we needed to worry about cycles, and saw one way of doing so (by marking each city as it was seen).

We mentioned briefly another way of finding a path: Start at the source, then find the list of all cities which are one step away, and then all the cities which are two steps away, then three steps, etc. At each step, we can see if the destination is included. If you think about it, this ends up guaranteeing that a shortest path is found.

 

How to write this function? Here's a handy helper:

;; map-append: (alpha --> list-of-beta), list-of-alpha --> list-of-beta
;; Map f to each element of inputs,
;; where f returns a list each time.
;; Return the appended results.
;;
(define (map-append f inputs)
  (foldr (lambda (frst rr) (append (f frst) rr))
         empty
         inputs))
  ; An alternate implementation, if you know "apply":
  ; (apply append (map f inputs))

Write the function
;; one-away-from: list-of-city --> list-of-city
;; 
(define (one-away-from srcs)
  ...)
Then, write a function which repeatedly calls one-away-from until it contains dest.
;; keep-looking: list-of-city, city --> list-of-city
;; 
(define (keep-looking srcs dest)
  (cond [..   ..]    ; Base-case
        [..   ..]    ; inductive case: keep-looking one-away-from srcs.
How does your code behave in the presence of loops? What if there is no path to the dest? How would you fix this?
Again, there are both accumulator and marking (mutating) approaches to the solution.

(The way keep-looking indefinitely repeats until a finish-condition, is often called a while-loop.)

The above approach, breadth-first search is nice, except that we don't actually return the path, oops! You can think of modifying the code to return how many steps are needed, to get from a given city to another. How might we go further, and keep track of the path? (How many paths need to be kept track of simultaneously? Where should we store that information? In the depth-first approach, where was this information stored?)

The above code can be found here: lect33.ss

 

Here is an example of a reasonably complicated breadth-first search.

What we want to do is to find all the shortest paths from a source to a destination in our flight paths problem from a couple of lectures ago (Lecture 31).

Our function should return a list of paths, which is a list of list of cities: (path src dest) --> list-of-list-of-cities

Following our basic template for generative recursion (see Lecture 29) our basic plan of attack is:

  1. Check for the trivial solution, which is that our source is the same as our destination.
  2. Otherwise,
    1. Save the path so far in the src's pathsTo field (which we've added since Lecture 31). The pathsTo field is a list of paths, which are all the paths from the src to that city. In the end, when we find the dest, these paths will be the final result.
    2. Set the src's seen? field to true so we don't loop back through it.
    3. Get all the "unseen" (not as yet seen) neighbors of the src city. This will eliminate any problems from loops and a src city that is its own neighbor.
    4. For safety's sake, any duplicate cities in the list of unseen cities should be removed. This keeps the function from calculating the same path more than once if a city has more than one link to another.
    5. Attempt to find all the paths from the unseen neighbors to the dest.

Let's think about what the function to find all the unseen neighbors of a city ("get-unseen-nhbrs") must accomplish:

  1. It must assume that the given city has already been seen, otherwise it cannot detect a loop to it.
  2. It must assume that it will be called multiple times at any given level on all the cities at that level. This restricts what it can and cannot set, or at times, modifies it to appending rather than setting.
  3. Any "seen" neighbor city should simply be ignored and not included in the resultant list of unseen cities.
  4. It cannot set the seen? field to true because this is a breadth-first traversal and there may be many other times, at this level, in which a given city may be examined as a viable path to the destination from via other cities. We cannot rule out the possibility that there are paths of equal lengths that may partially have the same cities --e.g. hou->la-slc & hou->nyc->slc or if you remove the la->reno link, hou->la->slc->reno & hou->nyc->slc->reno. This is why the pathsTo field must hold multiple paths.
  5. Any unseen neighbor city must have its pathsTo field augmented with the pathsTo of the given city with the given unseen neighbor city added to them. Note that one must assume that there are existing pathsTo in the nieghbor city due to processing by other parts of the breadth-first algorithm. Thus any new paths must be appended onto any existing paths.

Let us consider a function that will generate the next level of cities to work on (" getNextLevel"):

  1. Its input, in general, would be the list of cities at the current level.
  2. Its output would be the list of cities at the next level.
  3. The input cities, i.e. those at the current level, must be marked as "seen".
  4. The output would be all the unseen neighbor cities of all the input cities.
  5. There should be no duplicate cities in its output.

Note that the above specification points out a need for a couple of straightforward utility functions:

  1. A function to set the seen? field of all the cities in a list to true ("setSeen").
  2. A function to remove all the duplicate cities in a list of cities ("removeDups").

Now we can come back around and see if we can write Thus in general, at any given level of our breadth-first traversal of the city-to-city graph, we need a function that will find all the paths from a list of cities to the destination, "pathsToDest". This function derives directly from out main plan of attack above, which is in turn, based on the generative recursion template.

  1. Calculate if we have a solution yet.
    1. Is the list of source cities empty? If, so, we're done here because there's no paths to calculate.
    2. Otherwise, see if any of the cities in our source list is the same as the destination
      1. Add (append) the pathsTo from any matching cities to our result.
      2. If our result is non-empty (cons), then we have found a solution. Return the result.
  2. If we have no solution yet (i.e. the result from before was empty) -- keep looking:
    1. Get the cities in the next level from the our list of source cities, using the above getNextLevel function.
    2. Recur into that new set of source cites, i.e. (pathsToDest (getNextLevel loc))

That takes care of the inductive step, including the termination conditions. Now all we have to do is get this process started off.

The first level, i.e the src, is a little different than the other levels in that it starts with a single city, not a list of cities. Unfortunately, the solution is not as simple as just putting src into a list because the next level processing makes certain assumptions about the previous level, e.g. that all the cities have been seen and that they have their pathsTo field set to all the possible paths to that city. We end up with 2 choices on how to proceed:

  1. Do all the preliminary work by hand--setting seen? to true andsetting the pathsTo in the src to itself -- and then making the call to the next level, or
  2. Set up a dummy "zero'th level" city in which the src is its one and only neighbor. The pathsTo of this dummy city is a list containing the empty path. Now just put this dummy city into a list and make the inductive call.

The first technique is arguably easier to understand and conceptualized, while the second method does not duplicate any functionality.

Putting it all together, here's what we get, having put all of our helper functions inside of locals:


(define-struct city (name nhbrs pathsTo seen?))
;;
;; A city is a 

;; (make-city [symbol] [list-of-cities] [boolean])
;; where pathsTo is a list of list of cities, that are all the paths to this city from a source city-- to be used later, in path-finding.
;; seen? is a boolean that indicates if this city has already been processed--is used in path-finding as well.

; Initially no neighbors:
(define nyc  (make-city 'nyc  empty empty false))
(define la   (make-city 'la   empty empty false))
(define slc  (make-city 'slc  empty  empty false))
(define hou  (make-city 'hou empty empty false))
(define nome (make-city 'nome empty empty false))
(define reno (make-city 'reno empty empty false))

(define cities (list nyc la slc hou nome reno))


(set-city-nhbrs! nyc  (list la slc hou nome))
(set-city-nhbrs! la   (list reno slc nyc)) ;; comment out for more pathological hou->reno path
;; (set-city-nhbrs! la   (list slc nyc))  ;; uncommentfor more pathological hou->reno path
(set-city-nhbrs! hou  (list nyc la))
(set-city-nhbrs! slc  (list reno))
(set-city-nhbrs! reno (list slc))

;; path: city city --> list-of-list-of-cities
;; Takes a source city, src, and a destination city, dest,
;; and returns all possible shortest paths from src to dest.
;; The returned value is a list of paths, where each path
;; is a list of cities, starting wtih src and ending with
;; dest, defining a possible path from src to dest.
(define (path src dest)
  (local
      [;;removeDups:  list-of-cities --> list-of-cities
       ;; remove duplicate cities from the list-of-cities
       (define (removeDups loc)
         (local
             [;; isDup? city list-of-cities --> boolean
              ;; returns true if c is in cs
              ;; false otherwise
              (define(isDup? c cs)
                (foldr (lambda (a-c rr) (if (eq? c a-c) true rr)) false cs))]
           (foldr (lambda (a-city rr)
                    (if (isDup? a-city rr) rr (cons a-city rr)))
                  empty
                  loc)))
       
       ;; get-unseen-nhbrs: city --> list-of-cities
       ;; returns a list of all the neighbors of a-city
       ;; that have yet to be seen (the pathTo field is empty).
       ;; assumes that a-city has been seen already.
       ;; All cities in the return list will have their
       ;; pathTo set to the supplied city's pathTo appended
       ;; with themselves.
       (define (get-unseen-nhbrs a-city)
         (foldr (lambda (a-nhbr rr)
                  (cond 
                    [(city-seen? a-nhbr) rr];; neighbor has been seen, ignore
                    [else  ;; This neighbor is unseen
                     (begin
                       (set-city-pathsTo! a-nhbr  ;; set this neighbor's pathTo
                                          (append ;; combine all the possible paths to this city
                                           (map (lambda (a-path) ;; put this city at the end of all existing paths to a-city
                                                  (append a-path (list a-nhbr)))
                                                (city-pathsTo a-city) )
                                           (city-pathsTo a-nhbr))) ;; combine exisitng and new paths.
                       (cons a-nhbr rr))]))  ;; add it to the list 
                empty
                (city-nhbrs a-city)))
       
       ;; setSeen: list-of-cities --> list-of-cities
       ;; Sets the seen? field of all the cities in the list to true.
       ;; returns the list of cities (for chaining).
       (define (setSeen loc)
         (map (lambda (a-city)
                (begin
                  (set-city-seen?! a-city true) ;; set the seen? field to true
                  a-city))   ;; return the city -- map will rebuild the list.
              loc))
       
       ;; pathsToDest: list-of-cities --> list-of-list-of-cities
       ;; returns the list of the shortest paths from the supplied
       ;; list of cities, loc, to the dest.
       ;; All cities in loc are assumed to have their pathTo already set
       ;; i.e. non-empty
       (define (pathsToDest loc)
         (cond
           [(empty? loc) empty]  ;; Done! -- nowhere to go!  return empty path list
           [else
            (local
                [(define result  ;; see if we have an answer at this stage
                   (foldr (lambda (a-city rr) 
                            (cond 
                              [(eq? a-city dest) (append (city-pathsTo a-city) rr)]  ;; we have a complete path!
                              [else rr])) ;; keep looking
                          empty  ;; initial result is empty list
                          loc))]
              (cond        
                [(cons? result) result] ;; Done! -- Solution found.  Return result.
                [(empty? result) (pathsToDest (getNextLevel loc))]))]))    ;; not done, need recursive call to next level
       
       ;;getNextLevel: lict-of-cities --> list-of-cities
       ;; returns all the unseen neighbors of all the given cities.
       ;; Sets the given cities seen? to true.
       ;; Removes any duplicated cities from the returned cities.
       (define (getNextLevel loc)
         (removeDups (apply append (map get-unseen-nhbrs (setSeen loc)))))]
    
    ;;Create a dummy "zero'th" level city to start off the process.
    (pathsToDest (getNextLevel (list (make-city "" (list src) (list empty) false))))))

; Alternative code for the above single line that doesn't reuse existing code.
;     (cond
;       [(eq? src dest) (list (list src))]  ;; trivial sol'n -- we are at dest. Return simple list.
;       [else 
;        (begin                        ;; non-trivial sol'n
;          (set-city-pathsTo! src (list (list src)))  ;; the path to the src it itself.
;          (pathsToDest (getNextLevel (list src))))]))) ;; go via the neighbors
; 

----  We need a function to reset all the mutated fields in the cities ----
;; reset-cities: --> void
;; resets the pathTo field to empty
;; and seen? to false
;; in all the cities
(define (reset-cities)  
  (begin    
    (set-city-pathsTo! nyc empty)    
    (set-city-pathsTo! la  empty)    
    (set-city-pathsTo! hou  empty)    
    (set-city-pathsTo! slc  empty)    
    (set-city-pathsTo! reno  empty)
    (set-city-pathsTo! nome empty)
    (set-city-seen?! nyc false)    
    (set-city-seen?! la  false)    
    (set-city-seen?! hou  false)    
    (set-city-seen?! slc  false)    
    (set-city-seen?! reno  false)
    (set-city-seen?! nome false)))

The code can be downloaded here: breadthpath.scm (a function to nicely display the results is also included -- check it out!)

 

 

 

©2003 Stephen Wong