CS 1321X - Lecture 17 - October 16, 2003

CS 1321X - Lecture 17

State-Space Search


I.  More about networks in the real world

Network abstractions have even been used by popular publications to
explain what's going on between the characters in television shows. For
example, at the height of the popularity of the show "Twin Peaks"
several years ago, both People and Newsweek published very detailed
network representations of the relationships between the many
inhabitants of the town of Twin Peaks. I showed you all reproductions
of these diagrams, so I won't bother to repeat them in ASCII here
(whew!), and you could tell just by looking at them that these networks
are far from tree-like (i.e., there's no obvious hierarchy, and there are 
most definitely some cycles.)  But the fundamental ideas about organizing 
knowledge in terms of things and relationships between things are still 
there, as are the fundamental ideas about how to traverse these structures.

Oh, by the way, here's some more terminology...you'll also find
structures like these called "semantic networks" instead of "relational
networks", depending on how they're used, but you don't need to worry
about that much unless you take the artificial intelligence class.

But in summary, let's revisit the original question, "Why are we
getting so excited about these trees and/or networks?" As we've seen,
the answer is that we can model so many diverse things with them. In
just this brief time, we've seen how we can model the organization of
dictionaries, human memory (maybe), a small company, the interstate
highway system of a large country, the film industry, and the World
Wide Web itself, all using the same basic nodes-and-links
representation scheme. Furthermore, in so doing, we've shown that this
common thread runs through cognitive psychology, artificial
intelligence, object-oriented programming, and relational databases,
just to name a few areas of academic endeavor. 


II.   Graph search example

Here's a problem that shows up occasionally in homework assignments
or quizzes on occasion.  The solution should give you some idea as
to how we modify our depth-first search algorithm to accommodate
these "messy" networks.

Problem:  Below is a map (not to scale) of galaxies in the universe and
worm-hole connections between them:


             B --------- F
           / | \         | \
          /  |  \        |  \
         /   |   \       |   \
        /    |    \      |    \
       A     |     \     |      G
        \    |      \    |    /
         \   |       \   |   /
          \  |        \  |  /
           \ |         \ | /
             C --------- D
               \         |
                \        |
                 \       |
                  \      |
                   \     |
                    \    |
                     \   |
                      \  |
                       \ |
                         E


In the map above, galaxy A is connected by a worm-hole to galaxy B, and 
is also connected by a worm-hole to galaxy C.  Galaxy B is connected to 
galaxy A of course, but also has worm-hole connections to galaxies C, D, 
and F.  Galaxy C is connected...well, you get the idea.  Here is that 
same map of worm-hole connections, represented as an association list:


(define map
  '((a (b c))
    (c (e d b a))
    (e (c d))
    (b (c f d a))
    (f (b g d))
    (d (f e g c b))
    (g (f d))))


Before setting out on their travels, weary space commuters in the 
23rd Century would like to know if it's possible just to get from one 
place to another by making worm-hole "hops".  Your task is to write the 
Scheme code that will be running on the Palm Pilots of the distant future 
to help these commuters.  Call your Scheme program "hoppy".  The hoppy 
function takes three arguments:  the start galaxy, the end galaxy, and the 
map represented as an association list.  If it's possible to get from the
start galaxy to the end galaxy, the hoppy function returns a list
representing one candidate path from the start galaxy to the end
galaxy.  If a path can't be found, the function returns #f. 

Use depth-first search to find a path that meets the given constraints,
and employ abstraction to separate the high-level search algorithm from
the low-level details of accessing the data structure. 

(define map
  '((a (b c))
    (c (e d b a))
    (e (c d))
    (b (c f d a))
    (f (b g d))
    (d (f e g c b))
    (g (f d))))


(define (hoppy start goal map)
  (hoppy-helper (list start) goal map ()))  ;; initialize list of galaxies
                                            ;; to be explored and set path
                                            ;; from start to goal to be the
                                            ;; empty list

(define (hoppy-helper galaxy-list goal map path)

        ;; have we explored all galaxies and not found goal? return #f

  (cond ((no-more-galaxies? galaxy-list) #f)

        ;; have we found the galaxy we're looking for?  return the path

        ((found-goal? (get-first-galaxy galaxy-list) goal)
         (reverse (cons goal path)))

        ;; are we looking at a galaxy we've already looked at?
        ;; if so, we have a cycle, and if we continue on, we'll just
        ;; keep repeating the cycle, so call this function recursively
        ;; on the rest of the galaxies to be explored to get out of this
        ;; cyclical pattern

        ((found-cycle? (get-first-galaxy galaxy-list) path)
         (hoppy-helper (get-rest-of-galaxy-list galaxy-list) goal map path))

        ;; we didn't find the goal, and we didn't find a cycle, so
        ;; let's do two recursive function calls: the first using the
        ;; list of galaxies that are immediate neighbors to the one
        ;; we're currently looking at, and the second using the 
        ;; rest of the galaxies on the galaxy list

        (else (or (hoppy-helper (get-neighbor-galaxies 
                                 (get-first-galaxy galaxy-list) map)
                                goal
                                map
                                (cons (get-first-galaxy galaxy-list) path))
                  (hoppy-helper (get-rest-of-galaxy-list galaxy-list)
                                goal
                                map
                                path)))))


;; the abstraction wall is here: high-level algorithm above, implementation
;; details below

(define (no-more-galaxies? galaxy-list)
  (null? galaxy-list))

(define (found-goal? galaxy goal)
  (equal? galaxy goal))

(define (found-cycle? galaxy path)
  (member galaxy path))

(define (get-first-galaxy galaxy-list)
  (car galaxy-list))

(define (get-rest-of-galaxy-list galaxy-list)
  (cdr galaxy-list))

(define (get-neighbor-galaxies galaxy map)
  (cadr (assoc galaxy map)))


And when we finally get around to running this thing, we see that we 
don't necessarily get the shortest path possible between galaxies, but we 
do get one if one exists...we get the first one hoppy finds:

> (hoppy 'a 'd map)
(a b c e d)
> (hoppy 'a 'g map)
(a b c e d f g)
> (hoppy 'a 'a map)
(a)
> (hoppy 'a 'h map)
#f
>


III.  The statespace

The metaphor of searching a tree is also a convenient one for
describing the state of a process (i.e., a program in  execution).  The
state of a process changes over time, and at any given time the state
of a process is a little slice of its history.

At a very low level, the state of a process is described by the values
of the arguments being passed, the instruction being executed, and if
you're programming with side effects, the bindings of variables to
values.  (Obviously, it's easier to describe the state of a process if
you don't have to worry about side effects, as there's just that much
less to keep track of.)  However, thinking about state at this low
level becomes very tedious very quickly.  So, we might be better off 
using a higher-level abstraction in thinking about the state of a process. 
Consider, for example, a program to solve the 8-tile puzzle.  Instead
of thinking in terms of which instruction is being executed, the values
bound to arguments, and so on, we can look at the process in terms of
the state of the puzzle itself.  Thus, the initial state of the process
would be the initial state of the puzzle.  Say the initial state looks
like this:


                  2 8 3
                  1 6 4
                  7   5


We could move any of three tiles, the 7, the 6, or the 5, to 
generate the three possible next states from this one:


                  2 8 3
                  1 6 4
                  7   5
                   /|\
                  / | \
                 /  |  \
                /   |   \
               /    |    \
              /     |     \
             /      |      \
            /       |       \
          2 8 3   2 8 3   2 8 3
          1 6 4   1   4   1 6 4
            7 5   7 6 5   7 5


If we then choose, say, the lower leftmost state of those 
three new states, and generate the two possible next states 
from that one, we get this:


                  2 8 3
                  1 6 4
                  7   5
                   /|\
                  / | \
                 /  |  \
                /   |   \
               /    |    \
              /     |     \
             /      |      \
            /       |       \
          2 8 3   2 8 3   2 8 3
          1 6 4   1   4   1 6 4
            7 5   7 6 5   7 5
           / \
          /   \
         /     \
        /       \
       /         \
     2 8 3     2 8 3
       6 4     1 6 4
     1 7 5     7   5


Note that one of these new states is just a repeat of the initial state.  
We wouldn't want to explore that direction any further, because we'd just 
be doing work we've already done.

Now, if we think of the movement of tiles as the significant operators
in this process, we can describe the history of the process in terms of
puzzle boards and the operators necessary to get from one board to the
next.  And since the nature of the operators in this case are such that
only at most four new boards can be generated from any given board, we
can safely say that the current behavior of the process depends on its
history--the process couldn't have been in the current state without 
having just been in one of a very few previous states.

If we keep applying operators (i.e., moving tiles) to the leftmost
board in the tree, we're going to get a depth-first search.  But we're
not searching some pre-existing data structure; instead we're searching
something that's being "built" as the program executes.  This something
is called a "state space" (or a "problem space"), and our hypothetical
8-tile program is performing a "state-space search" by following a
depth-first search algorithm.

A state-space is defined as the set of all possible states generated by
the repeated application of a finite set of operators (or operations,
or transformations, or moves...they're all the same thing in this
context) to some initial state.  In performing a state-space search,
the intention is usually to find a sequence of operators that gets one
from the initial state to some goal state.  In the case of the 8-tile
puzzle, that goal state might be:


                  1 2 3
                  8   4
                  7 6 5


Why generate the state space at run-time, and not just have it all built 
in advance?  For some applications, that might not be much of a problem.  
For example, in the 8-tile puzzle, the number of different ways to arrange 
the tiles isn't overwhelming.  At most, it might be around 9!, before 
subtracting impossible states.  On the other hand, if you were working 
on a program that could play a decent game of chess, and you wanted to 
pre-build a data structure that was comprised of all possible boards, 
you'd want to make sure that you set aside a little disk space to store 
the approximately 10^120 (i.e., 1,000,000,000,000,000,000,000,000,000,000,
000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,
000,000,000,000,000,000,000,000,000,000,000,000) different boards that 
are possible.  Or maybe you'd be better off writing your program to 
generate just those boards that were relevant to the specific chess 
game it was playing at that particular time, and not worry about the 
rest of them.  Stay tuned.



Copyright (c) 2003 by Kurt Eiselt.  All rights reserved, with 
the exception of stuff that belongs to somebody else.

Last revised: October 20, 2003