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
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.
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 built 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 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.
We didn't mention it in class, but 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.
It's a whole lot like the "find-relationship" function from
last week's homework, except you search states that are generated
as the process moves forward as opposed to searching some previously-
existing data structure. So if you got your "find-relationship" function
to work in some non-obtuse way, you're well on your way to mastering
the homework assignment that will be posted tonight.
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: November 3, 1998