Finding the Shortest Path by Breadth-first Search


This little program finds the shortest path through a network from one node to another. For example, when the network represents a map, this program finds the shortest route from one city to another.

This example is based on pps. 51 -- 53 in Paul Graham, ANSI COMMON LISP, Prentice-Hall 1996. The following explanation is paraphrased from Graham.

A network is represented by an association list, where the first element of each sublist is a node and the rest of each sublist is the list of nodes which are adjacent to the first node.

Path-demo is the driver that prompts for the file name that contains the network, the starting node, and the ending node.

Shortest-path takes a start node, a destination node, and a network, and returns the shortest path, if there is one.

The program works by searching the network breadth-first. You have to maintain a queue of unexplored nodes. Each time you get to a node, you check to see if it is the one you want. If not, you append each of its neighbors to the end of the queue, then take a node from the front of the queue and continue the search there.

The code represents a slight complication of this idea. We don't just want to find the destination, but to keep a record of how we got there. So instead of maintaining a queue of nodes, we maintain a queue of the paths we've followed.

The function bfs does the searching. Initially shortest-path calls bfs with one element in the queue, a path representing the start node with no history.

If the queue is empty, bfs returns the empty list to indicate that no path could be found. If there are still nodes to search, bfs looks at the element on the front of the queue. If the car is the node we're looking for, we've found a path and we just return it, reversing it for readability. If we haven't found the node we're looking for, we add each of the current node's neighbors to the end of the queue. Then we call bfs recursively to continue searching the rest of the queue.

By always putting deeper nodes at the end of the queue, we ensure that the network gets searched one layer at a time. Because we search breadth-first, the first path we find will be the shortest, or one of the shortest.

This is not the fastest way to search a network, but it does demonstrate the versatility of lists. We use a list of nodes to represent a path, a list of paths to represent the queue used in the search, and an association list to represent the network itself.

Here is the code. A network follows.

(define (path-demo)
  (display "File name: ")
  (let ((fname (read-string)))
    (let ((infile (open-input-file fname)))
      (let ((routes (read infile)))
        (display "Start: ")
        (let ((start (read-string)))
          (display "Finish: ")
          (let ((finish (read-string)))
          (for-each (lambda (x) (display x) (display " "))
                    (cons "Route:" (shortest-path start finish routes)))))))))
  
  (define (shortest-path start end net)
    (bfs end (list (list start)) net))
  
;; Breadth-first search
  (define (bfs end queue net)
   (print queue) (newline) (newline) ; entertainment
    (if (null? queue)
        '()
        (let ((path (car queue)))
          (let ((node (car path)))
            (if (equal? node end) ;; Graham used CL eql
                (reverse path)
                (bfs end 
                     (append (cdr queue)
                             (new-paths path node net))
                     net))))))
  
  (define (new-paths path node net)
    (map (lambda (n) (cons n path)) (cdr (assoc node net))))

Here is a sample network that represents a map of cities around Puget Sound. On my system it is stored in C:\scheme\puget-cities.txt. Nodes are represented by quoted strings to handle cities like Gig Harbor whose names include more than one word.

(("Bremerton" "Seattle" "Gig Harbor")
 ("Gig Harbor" "Bremerton" "Tacoma" "Olympia")
 ("Olympia" "Gig Harbor" "Tacoma")
 ("Tacoma" "Olympia" "Gig Harbor" "Seattle")
 ("Seattle" "Bremerton" "Tacoma"))

To run the demo execute (path-demo) then answer the prompts asking for the map file name, the starting node and the ending node.

Here is a sample program run.

(path-demo)
File name: C:\scheme\puget-cities.txt
Start: Seattle
Finish: Olympia

 ... intermediate results print out ...

Route: Seattle Tacoma Olympia

Jon Jacky, jackyj@evergreen.edu