CS 2360 - February 12, 1998

Lecture 12 -- State Space Search


The state space

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 
operations in this process, we can describe the history of 
the process in terms of puzzle boards and the operations 
necessary to get from one board to the next.  And since the 
nature of the operations 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 operations (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 
operations to some initial state.  In performing a state-
space search, the intention is usually to find a sequence of 
operations 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.


Examples of state-space search in the real world

The state-space search is used in a lot of ways by lots of folks.
For example, a compiler has a component called a parser which 
decomposes a high-level instruction into its component parts.  
But these instructions can be ambiguous, so the parser must 
make decisions about how various symbols (known as "tokens" 
in compiler world) are being used.  How that decision is made 
depends on what the parser has already seen; in other words, 
the next possible state of the parsing process depends on the 
history of the previous states.

The parser reads the input from left to right, making 
"guesses" as it goes.  If the sequence of guesses leads to a 
structure for an instruction that's not legal, the parser 
will backtrack and systematically try new guesses, just like 
a depth-first search algorithm.  If no combination of guesses 
works for the parser, you'll get a "syntax error" message.  
These things are sometimes called "recursive descent 
parsers", and you'll get to study these in your compiler 
course, if you're brave enough to volunteer for it.

The same sorts of ideas are used to get computers to 
understand English and other natural languages.  In fact, an 
entire company was founded on this idea.  A guy named Gary 
Hendrix at the University of Texas wrote a PhD thesis on 
parsing English back in the late 60's or early 70's.  He 
later took some of those same ideas and build an interface to 
a simple database system -- an interface that could accept 
data base queries in English (or at least a subset of 
English).  He called the whole thing "Q&A", it ran on PC 
compatibles, and it sold off the shelf at computer stores 
for about $300 a copy.  This product was one of the first, if 
not the first, offered by the company Hendrix co-founded, 
which is called "Symantec" -- a company which most of you Mac 
or PC owners know about, since it has swallowed up all sorts 
of other software vendors.  Hendrix is now a zillionaire, and 
the moral to this story is that state-space search can make 
you rich.

As we mentioned in class, evolutionary biologists think of 
all of us (and I mean *all* of us) as the bottom layers of 
nodes on a very big state space.  Those of us who don't have 
any children are the leaves on a very very big tree (well, 
it's not exactly a tree, but you get the idea).  Some of us 
will generate new states (our kids) and others of us won't.  
Each state presumably brings humanity slightly closer to some 
lifeform that is perfectly adapted to the environment.  (If 
only we could get the environment to stop changing....)

Finally, as we demonstrated via our Calvin and Hobbes 
example, state-space search is a nice little metaphor for how 
we lead our lives:  every decision we make is based on the 
chain of decisions leading up to that point.  As Calvin and
Hobbes illustrated however, in life, unlike in your computer, 
there's often no backtracking possible when you make a bad decision.


A state-space search algorithm (depth-first)

Here's a very sketchy, high-level depth-first state-space 
search algorithm that looks just like search algorithms that 
you've seen already, except that it generates what is to be 
searched as it goes, as opposed to searching some pre-existing 
data structure:

state-space (initial-states, goal-state, operators)

  1.  look at the first (leftmost) initial-state
  2.  if that state is the goal-state, then return success
  3.  if that state isn't the goal-state, then generate all
      possible new states from that state by applying the
      set of operators to that state
  4.  if there aren't any new states generated by applying
      those operators, then return failure
  5.  call state-space with this new list of states passed as
      the initial-states argument, and if that succeeds then
      return success else...
  6.  call state-space with the old list of initial states
      that remained after you stripped off the first
      initial-state in step 1, and if that succeeds then
      return success else...
  7.  return failure

In step 3, you'd like to check all the new states to see if 
you've explored them before.  You do that by keeping track of 
the sequence of states that was generated in going from the 
very first state to where you are now, and then comparing 
that list to the set of new states you just generated.  If 
there are any duplicates, be sure to eliminate them from the 
set of new states.

If you can implement something like this now, the next homework
assignment will be a breeze.  Sort of.


Making your search smarter

Searches like what we've seen so far are, in a word, dumb.  
They don't know which next state might be any better than any 
other next state.  These searches can be methodical (e.g., 
look at the first on the list) or random (e.g., Calvin's 
decision: "Arbitrarily, I choose left.").  These searches 
settle for finding the goal state, but they don't care about 
how many steps it takes to get from the initial state to the 
goal state.

Usually, however, we don't have time to burn.  We'd like to 
strive to find the goal state in as few steps as we can.  
That is, we'd like to try to find the "optimal path" from the 
initial state to the goal state, and we can help ourselves 
out here if we can put a little more "intelligence" into our 
search.

One time-honored way of doing this is to find a method to 
measure the "goodness" of a state -- that is, to determine 
how close a given state is to the goal state.  If we could 
make that evaluation consistently and correctly, then when we 
look at a list of states in trying to decide which to use 
next to generate new states, we could pick the state closest 
to the goal, instead of just picking the first one we see or 
picking one at random.

Most of the time, though, such measurements of a state's 
goodness are just estimates.  If the estimate is wrong, you 
could spend a lot of time and effort searching paths that 
will never get you to the goal, or at least that will give 
you less optimal solutions.  The better the ability to 
estimate goodness, the better is the chance for optimality.  
But unless the estimate is always right, there's no guarantee 
of success.  These measures of goodness are one example of 
something called "heuristics":  techniques that aid in the 
discovery of solutions to problems most of the time, but 
don't guarantee that they'll lead to solutions all of the 
time.


Heuristics and the 8-tile puzzle

Let's look again at the 8-tile puzzle we saw earlier.
There we described a dumb, exhaustive, brute-force, depth-
first search for finding the goal state.  Could you do 
better?  Probably yes.  If you could come up with a way to 
estimate how close any given arrangement of tiles was to the 
goal, you could always choose to explore the state that was 
nearest the goal.  To do this, you'd have to figure out a way 
to codify the metrics for this evaluation in such a way that 
a computer could use them.  One heuristic might be to just 
count the number of tiles that are in the place they belong.  
So if your goal state looks like this:

                  1 2 3
                  8   4
                  7 6 5

and your start state followed by the next possible states 
looks like 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

          score   score   score
            3       6       3

which of these next states is closer to the goal using our 
heuristic?  The middle state has six tiles in the right 
place (this assumes that we're going to count the empty space as
a tile), while the other two states have only three tiles in 
the right place.  So for our next step in the search, we'd 
choose to generate all the states possible from that middle 
state.  Then we'd apply our evaluation heuristic again, and 
so on.  Of course, we could get more sophisticated with our 
heuristic measures.  For example, we could try to estimate 
how many moves it would take to get all the tiles in their 
appropriate places instead of just counting how many were 
already in the right place.  That might give us a better 
measure of goodness, or it might just cause us to spend extra 
time computing the goodness without any real return on the 
investment, or it might just completely mislead the search.  
We'd have to play with it for awhile to see if it would help 
us.



Copyright 1998 by Kurt Eiselt.  All rights reserved.

Last revised: February 16, 1998