The following content is provided under a Creative
Commons license.
Your support will help MIT OpenCourseWare continue to
offer high-quality educational resources for free.
To make a donation or view additional materials from
hundreds of MIT courses, visit MIT OpenCourseWare at
ocw.mit.edu.
PROFESSOR: Hello.
Welcome.
Today we're going to talk about one last new topic which
has to do with search.
So as you remember, we're working on our last topic.
The last topic was probability and planning.
Last lecture we talked about probability.
Mostly we focused on Bayes' theorem, Bayes' rule.
That was a way of updating our belief about some situation
based on new information.
And this week in Design Lab 12, you'll get a chance to use
that in a robot application.
The idea to Design Lab 12 is going to be that a robot is
pedaling along a corridor with some
obstacles off to its left.
And the idea will be, you don't know where the robot is,
but the robot will be able to estimate where it is by the
signals that it receives from its left-facing sonars.
So this is a very realistic type of
state-estimation problem.
The idea is going to be that at any given time, t, you have
access to your previous belief at time t minus 1.
And you have access to a new observation, which is the
sonar to your left.
And based on those two bits of information, you will update
your belief.
Which means that when you start out you'll have no idea
where you are, but that situation should
improve with time.
Ok.
So that's what we're going to do in Design Lab 12.
Today we're going to blast ahead and think about the
other important topic, which is search.
So we're going to think about planning.
We're going to be planning ahead.
We're not going to just react to the situation
that's given to us.
We're going to try to figure out what's the right thing to
do in the future.
And to do that, we're going to think about all the things we
could possibly do, search through that space, and figure
out the one that's, quote, "best." And we'll have to
define "best" somehow.
Just to get going, I want to show you a very simple kind of
a search problem.
This is called the eight puzzle.
The idea is to make a plan to go from this configuration,
which we'll call the state, to this configuration, which
we'll call the goal state.
And on each move, you get to move one of the tiles into the
free space.
So I could move the 8 to the right or the 6 down.
Those are the only two things I could do in the start state.
So I have to make up my mind which of those I
would like to do.
And I would like to believe that ultimately, after a
series of moves, I'm going to be able to control this state
into that state.
And you can imagine guessing.
And we'll estimate in a moment how big is the guess space.
I mean, if the guess space only had like four elements in
it, guessing's a fine strategy.
If the guess space has a lot more elements than that,
guessing's probably not a good idea.
So I previously ran our search algorithms, the ones that
we'll develop during lecture, on this problem.
And here's the solution that our search
algorithm came up with.
It's not exactly what you might do the first time you
touched it.
OK, I made it.
If you were counting, I made 22 moves.
The question is, how difficult was that problem and how good
was that solution?
Was that a good solution or a bad solution?
Is there a better solution?
How much work did I have to do in order to
calculate that solution?
To get a handle on that, let's start by
asking a simple question.
How many configurations are there?
I got there in 22.
What was the space of things I had to look through?
How many different board configurations exist?
So think about that for 20 seconds.
Talk to your neighbor.
And figure out whether it is 8-squared, 9-squared,
8-factorial, 9-factorial, or none of those.
So how many bar configurations do you see?
Raise your hand, show me a number of fingers, so I can
figure out roughly how people were-- that's excellent.
Very good participation, and nearly 100% correct.
The answer is number (4).
You can think about this.
What if you took all the tiles out and threw them on the
floor, and then put them in one at a time?
Well, you would have 9 possibilities for where you
wanted to put the first one.
Then you would have 8 possibilities for where you
wanted to put the second one.
Then you'd have 7 possibilities for where you
put the third one, et cetera, et cetera.
Even though the space doesn't have a number on it, it still
sort of counts.
And so you end up with 9-factorial.
And the point is, 9-factorial is a big number.
9-factorial is 362880.
So if you thought about simply guessing, that's probably not
going to work all that well, right?
Even if you guessed, you have on each--
there's a third of a million different configurations that
you have to look at.
And that's if you didn't lose track of things.
If you lost track of--
Oh, my.
It's coming up--
almost, anyway.
It looks like it's chopped off at the top.
So ignore that for now.
Look over here.
So even if you didn't confuse yourself, there's a space of a
third of a million things to look at.
And if you confused yourself, there's even more.
So it's not a huge problem by computer science standards.
But it's certainly not a trivial problem.
It's not something that you can just guess and get right.
So what we want to do today is figure out an algorithm for
conducting a search like that.
We'd like to figure out the algorithm, analyze how well it
works, optimize it, and try to find out a way to find the
best solution, where "best" for this particular problem
would mean minimum path length.
So figure out the best solution by considering as few
cases as possible.
Obviously, if you enumerate all the
cases, that should work.
The problem is, it will be interesting to solve problems
where that enumeration is quite large.
Even here, the enumeration is quite large.
So let's think about the algorithm.
And I'll think about the algorithm by way of an even
simpler, more finite problem.
What if I thought about a grid of possible locations
where I could be.
Maybe this is the intersections
of streets in Manhattan.
I want to go from point A to point I. What's the
minimum-distance path?
I hope you probably can all figure that out.
What I want to do is write an algorithm that
can figure that out.
And then if we write the algorithm well, we'll be able
to use it for the tile problem, which is not quite so
easy to do.
The way we're going to think about doing that is to
organize all of our possible paths through
that maze, in a tree.
So if I started at A, I have a decision to make.
I could either go to B or D.
Then if I went to, say, B, I could either then go to A or C
or E. I've organized them alphabetically for no
particularly good reason.
Just--
I needed some order.
Then if I went from A to B to A, say, then I could either go
from A to B or D. That's illustrated here.
So the idea then-- ah, it works.
So now it looks like they're all three working.
So the idea is, think about the original problem.
The original problem is find the shortest path
through some grid.
I want to go from A to I. And I'll think about all the
possible paths on a tree.
Then the problem is that for the kinds of problems we're
going to look at, that tree could be infinite in length.
Oh, that's a bummer.
That means that the strategy of building the tree and then
searching it is probably not a good strategy.
So what we'll do instead is, we'll try to write the
algorithm in such a way that we construct the tree and
search for the best solution all in one pass.
Then hopefully, if we find a solution in some finite number
of steps, we'll only have built part of the tree.
But we'll have built the part that has the answer.
The idea, then, is going to be, think about what is the
path we want to take, by thinking about the tree of all
possible paths.
But what we want to do is write code that will construct
the tree on the fly, while it's considering how good were
all the different nodes.
Ok.
So how are we going to do that?
We'll be working in Python, not surprisingly.
We'll represent all the possible locations.
We'll call those states.
So the problem will have states A, B, C, D, and we'll
just represent those by strings.
That makes it flexible.
That makes it arbitrary.
Then we'll think about transitions, not by
enumerating them.
Remember, we don't want to enumerate them, because there
could be infinitely many of them.
So how's the other way we could do it?
Well, we'll embody that information in a program.
We'll write a procedure called "successor" that will, given
the current state and action, figure out the next state.
So that's a way that we can incrementally build the tree.
So imagine here, if I started in A and I executed action 0
or 1, I would end up in B or D, respectively.
So I tell you the current state and the current action,
and the successor program then will return
to you the new state.
That's all we need to construct the tree on the fly.
Then, to specify the particular problem with
interest, I have to tell you where you start.
So I have to define initial state.
And I have to tell you where to end.
I could just tell you the final state.
But in some of the problems of the type that we will want to
do, there could be multiple acceptable answers.
So I don't want to just give you the final state.
I'll give you a test.
I'll give you another procedure, called "goalTest."
And that goal test, when passed an input which is a
state, will tell you whether or not you reached the goal.
That way, for example, all the even-numbered squares could
satisfy the goal, if that were the problem of interest.
Or all the states on the right could satisfy the goal.
It's just a little bit more flexible.
The idea, then, is that in order to represent that tree,
we'll do it by specifying a procedure called successor and
specifying the start state and the goal test.
So here's how I might set that up for the simple Manhattan
problem that I showed-- that I started with.
So I want ultimately to have something called successor
that eats a state and an action.
I've built the structure of Manhattan into a dictionary.
The dictionary lists for every state--
A, B, C, D, E, F, G, H, I--
for every state, it associates that state with a list of
possible next states.
So if I'm in A, I could next be in B or D. I could next be
in B or D.
I've organized these arbitrarily in alphabetical
order so I can remember what's going on.
So the next states are all in alphabetical order.
The number of next states depends on the state.
I'm not going to worry about that too much.
I'm just going to specify the action as an integer--
0,1,2,3 --
however many I need.
So the possible actions are taken from that list.
The possible action might be do action 0, do action
1, do action 2.
So if I did action 2 starting on state E, I would go to--
so action started at 0 --
so 0, 1, 2 --
I would go to state F. OK?
Is that all clear?
The initial state is A, and the goal state is, return S
equal to I. So if S is equal to I, it returns True.
If S is not equal to I, it returns False.
I'm not quite done.
That's enough to completely specify the tree, but now I
have to build the tree in Python.
Not surprisingly, from our object-oriented past, we will
use an object-oriented representation for that tree.
So we'll specify every node in the tree as an instance of the
class SearchNode.
SearchNode is trivial.
SearchNode simply knows, what was the
action that got me here?
Who's my parent?
And what's my current state?
So when you make a new node, you have to tell the
constructor those three things.
What was the action that got me here?
What's my current state?
And who is my parent?
Knowing the node, you're supposed to know the entire
path that got you here.
So we'll also add a method which reports the path.
So if I happen to be in node E, my path ought to be I
started in A, I took action 0 and got to B, and then I took
action 2 and got to E. This subroutine is
intended to do that.
If my parent is "none," which will happen for the initial
state, simply report that the path to me is none, A.
However, if I'm anybody other than the initial node, if I'm
other than the initial node, then figure out the
description of the path to my parent, and add the action
that got me here, and my state.
So that's what this is.
OK, so what are we doing?
We specify a problem by telling you the successor
function, the start state, and the goal test.
Then we provide a class by which you can build nodes to
construct, on the fly, the search tree.
Now we're ready to write the algorithm.
Here's the pseudocode for the algorithm.
What do we do?
We initialize--
so we're going to be doing a s-- oh,
this is very confusing.
I'm trying to solve a search problem.
To solve the search problem, I'm going to
search through the tree.
So I'm going to think about the state of my search through
the tree by way of something we'll call the agenda.
Very jargon-y word.
I completely apologize for it.
I didn't invent it.
It's what everybody calls it.
Sorry.
The agenda is the set of nodes that I'm
currently thinking about.
So I'll initialize that to contain the starting node.
Then I'll just systematically keep repeating the same thing
over and over again.
Take one of the nodes out of the agenda, think about it,
replace that node by its children.
While I'm doing that, two things are supposed to happen.
I'm supposed to construct the search tree.
But I'm also going to be looking over my shoulder to
see if I just constructed a child who is the answer.
Because if I just constructed the answer, I'm done.
Ok.
So initialize the agenda to contain just
the starting node.
Then repeat the following steps.
Remove one node from the agenda.
Add that node's children to the agenda.
And keep going until one of two things happens.
Either you found it--
goal test returned True--
or the agenda got empty, in which case there
must not be a solution.
If I've removed all of my options and still haven't
found anything, then there's no solution.
Ok.
So what's the program look like?
It's actually remarkably simple, especially when you
think about just how hard the problem is.
Imagine if you wanted to do that tiles problem with a very
simple-minded "if this then this, if this then this."
We're talking about a third of a million ifs, right?
That's probably not the right way to do it.
This program is going to end up being about this long.
It'll fit on this page.
And it's going to be able to handle that case, or even
harder cases.
Define the search procedure.
The search procedure is something that's going to take
the initial state, the goal test, the possible actions,
and the successor sub routine, the successor procedure.
That's everything you need to specify the problem.
And it's going to return to me the optimal path.
First, step (1).
Initialize the agenda to contain the start node.
I want to put the start node in.
Well, there's a chance--
I want this procedure to be general purpose--
there's a chance that that start node is the answer.
So take care of that first.
If you're already there, return the answer.
The path to the answer is me.
I'm trying to create the agenda.
I'm trying to put the first node into the agenda.
There's a chance that first node is the answer.
If that first node's the answer, return the path to me.
Which is, take no action.
You're here.
But that's not likely to be the case for the kinds of
questions we ask, in which case we will create a list
that contains one node, which is the node that represents
the start node.
Then, repeat 'remove a node' -- which
we'll call the parent--
from the agenda.
And substitute --
replace that node that we pulled out of the agenda --
replace that with the children.
While not empty of agenda--
empty is some kind of a pseudo-routine that I'm going
to fill in, in a minute--
while the agenda is not empty, get an element out of the
agenda, which we'll call the parent.
Then I want to think about all the children.
Well, there's a list of possible actions.
So for a in actions, do the following things.
Each parent can have multiple children, one for every
possible action.
So for a in action, figure out what would be the new state.
The new state is just the successor of the parent state.
Remember, the parent is a node, right?
The parent is a node.
We're constructing nodes in the search tree.
But nodes know their state.
So figure out the new state, which is the successor of my
parent, the guy that I pulled out of the agenda.
Make a new node, which corresponds to this child.
Then ask the question, did the new state
satisfy the goal test?
If it did, the answer is the path to the new node.
Ok.
So create a new state, which is the successor of the parent
under the given action A. Create a new node.
See if it's the end.
If it is, just return, I'm done.
Return from search.
Otherwise, add it-- again, one of these pseudo-procedures.
We'll fill that in, in a minute--
add the new node into the agenda.
There's several things that could happen
when I run this loop.
If the node has no children, I will take out the parent and
not put anything back in.
If the node has multiple children, I could take out one
node and put in more nodes than I took out, so the agenda
could get longer.
So the agenda could either increase in length or decrease
in length as a result of spinning around this loop.
Also, we could either identify a goal or fail
to identify a goal.
So as it's increasing and decreasing, we either will or
won't find an answer.
Ok.
Now the trick--
the only thing that makes this complicated--
is that order matters.
So those pseudo-operations, whatever they were--
get element and add--
exactly how I get element and exactly how I add it to the
agenda affects the way I conduct the search.
Let's think of something very simple.
Let's always remove the first node from the agenda and
replace it by its children.
So pull out the first node.
And put back into the beginning of the agenda the
children of the first node.
How?
I would start out in step (0).
I would put the start node into the agenda.
So now there's one element in the agenda, the start node.
Then, on the first pass through the loop, I
would pull out that.
That's the first node in the agenda.
There's only one node in the agenda.
That is the first one.
Pull out the first one and replace it by its children.
Its children are AB and AD.
I'm representing the nodes in this notation by the path,
because the same state can appear multiple
times in the tree.
Notice that I could walk ABAB --
which would correspond to the same state being
repeated in the tree.
So I can't, when I'm writing it down here, represent the
node by a state.
But I can represent a node by a path.
So on the first pass through the loop, pull out the first
item in the agenda, which is A, and push
back that A's children.
Well A's children are AB and AD.
Ok.
So now on the second pass, the rule is pull out the first guy
and replace it by the children.
Now the first guy is AB.
So I'm here.
So pull that guy out and replace him by his children.
His children are ABA, ABC, and ABE.
AD is left over.
The number of elements in the agenda got bigger.
Next step, pull out the first item in the agenda, replace it
by its children.
The first item in the agenda is ABA.
The children of ABA are ABAB and ABAD.
Ok.
Notice the structure of what's going on.
Ignore the stuff on the bottom, and just watch the
picture on the top.
So I start by putting A in the agenda, then its children,
then its children, then its children.
So when I implemented the algorithm--
take out the first and replace it by its children--
I'm searching along the depth first.
I'm going deeper and deeper into the tree without fully
exploring all the horizontal spaces.
So I'm tracing a line down that way.
If you imagine this tree--
I've only represented the first three layers of nodes
here-- this tree goes on forever.
It's an infinite tree, because you can walk around in that
Manhattan grid forever.
There's no limit to how long you can walk around.
So although I'm only listing the first three, the tree, in
principle, goes on forever.
And this algorithm will have the feature that it walks
along the left edge.
We call that depth-first search because we're exploring
depth first, as opposed to breadth.
That results because of our rule.
The rule was, replace the first node by its children.
Let's think about a different rule.
Let's replace the last node by its children.
We start by initializing the agenda to the node that
represents the start state.
So that's the path A. Then pull out the
last node in the agenda--
that's A--
and replace it by its children.
Its children are still AB and AD, just like before.
Now the answer differs from the previous answer.
Because when I pull out the last node, I'm pulling out AD
now, instead.
And now I replace AD by its children, which
are ADA, ADE, ADG.
Repeat, and what I've got is a different, but still
depth-first search.
So I've looked at two different orderings--
pull out the first node from the agenda and replace it by
children, pull out the last node and
replace it by its children.
Both of those algorithms give an exploration of the decision
tree searching out depth first.
So it's going to try to exhaustively go through the
entire depth before it tries to explore the width.
As an alternative, think about a slightly
more complicated rule.
Remove the first element from the agenda and add its
children to the end of the agenda.
So initialize it with the start state, A. Pull out the
first element from the agenda-- that's A--
and replace it by its children, which is AB, AD.
Now pull out the first guy--
the first guy is AB--
and put its children at the end.
It's children are ABA, ABC, ABE--
and they are now put at the end.
So that on the next step, I'll pick up AD-- the guy at the
beginning--
and put AD's children at the end.
et cetera, et cetera, et cetera, et cetera,
et cetera, et cetera.
The idea being-- and now, pay no attention to the bottom for
a moment and just think about the pattern that
you see at the top.
In this order, where we remove the first node and put its
children at the end of the agenda, has the effect of
exploring breadth first.
So we call that a breadth-first.
so the idea is, we got this generic set of tools that let
us construct search trees.
But the order by which we manipulate the agenda plays a
critical role in how the search is conducted.
And the two that epitomize the two extreme cases are, what
would happen if I replace the last node by its children?
Or what would happen if I remove the first node and put
its children's at the end?
Those two structures have names because
they happen so often.
We'll call the first one a stack and the
second one a queue.
The stack-based is going to give us depth first.
The queue-based is going to give us breadth first.
So, stack.
How do you think about a stack?
You think about a stack by saying, OK, I've got a stack.
A stack is like a stack of dishes.
So here's my table, and I'm going to rack my dishes up.
I'm going to put them on a stack.
So I make a stack.
OK, I made the stack--
push a 1, push a 9, push a 3.
Push a 1, push a 9, push a 3.
That's how I do a stack.
Then pop.
When I pop, the 3 comes out.
Then pop, then the 9 comes out.
Then push a minus 2.
Then pop.
Now the minus 2 comes out.
OK?
It's stack-based.
So the last in becomes the first out.
That was the rule that we wanted to have for the
depth-first search.
It's very easy to implement this.
We can implement it as a list.
All we need to do is be careful about how we implement
the push and pop operators.
So if we set up the push operator to simply append to
the end, and then pop ordinarily pops from the end,
we'll get the behavior of a stack.
That gives me, then, the rules that I would want to use for
those procedures, the get element an add.
I will use these stack-based operators.
The other alternative is a queue.
A queue is different.
A queue is like when you're waiting in line
at the Stop & Shop.
The queue is, I've got this queue here and I've got the
server over here.
The first person who comes into the queue--
so say I push one.
So now 1 goes into the queue.
Then another person walks up while he's--
the second person lines up behind the first person.
Then I push a 3.
But the way the queue works is that when I pop the next
person off the queue, I take the head of the line.
So the 1 comes out.
If I pop again, the 9 comes out.
If I then push a minus 2 and pop, then the next person in
the queue comes out.
And it's like that.
It's queue based versus stack based.
And the queue based is the one that we want to do for a
breadth-first organization.
And the implementation of a queue is very trivially
different from the implementation for a stack.
The only difference is that I'll manipulate the list by
popping off from the head of the queue.
So pop takes an optional argument, which when
0, tells you the--
the argument tells you which element to pop.
So when you specify the zero-th one, it takes it from
the head of the queue.
That makes it very easy now to replace the pseudo-procedures
with real procedures.
If I wanted to implement a depth-first search, I would
replace the "create a list that contains the agenda" with
"create a stack that will contain the agenda." So create
a new stack, the agenda is a stack.
And then rather than simply sticking the node--
the start node--
into a list, I will push it into the stack.
So agenda is a stack.
Agenda.push, the initial node.
And then every time I want to get a new element out, I'll
agenda.pop it.
And every time I want to put something into it, I'll
agenda.push it.
Other than that, it looks just the same as the pseudocode
that I showed earlier.
So there is an implementation, then, for
a depth-first search.
If I wanted instead to do breadth, it's trivial.
Change the word "stack" to the word "queue." Now create an
agenda that is a queue, but queues have the same
operations--
push and pop and empty--
that stacks have.
So nothing else in the program changed.
All I needed to do is change the structure of the thing
that's holding the agenda.
Everything else just follows.
Ok.
So that's everything we need, right?
Now what I want to do is think through examples and think
about the advantages and disadvantages of different
kinds of searches.
And I want to go on to the second step that I raised in
the first slide.
I want to think about, how do I optimize the search?
As I said, even that simple little tile problem, even the
eight puzzle--
eight sounds easy, right?
Even the eight puzzle had a third of a
million different states.
I don't necessarily want to look through all of them.
I want to think now about these different search
strategies, and how optimal are they relative to each
other, and are there ways to improve that?
Now some of you may have noticed that all of these
paths don't seem equally good.
So take a minute.
Think about it.
Remember the problem.
The problem was this walk around Manhattan problem.
I wanted to go from A to I. This was the tree of all
possible paths from A to I. What I'd like you to do is
think about whether all of those paths are important.
Could we get rid of some of them?
So the question is, can I throw away some of the
terminal nodes?
Notice that I'm using the word "terminal" kind of funny here.
The tree keeps going.
The tree is actually infinite in length.
So by "terminal," I just mean this row three.
So could I throw away some of the nodes in row three?
And in particular, how many of them could I throw away?
0, 2, 4, 6, or 8?
or Raise your hand with the funny coding.
And the answer is--
come on, come on.
Raise your hands, raise your hands.
Blame it on your neighbor.
That's the whole point.
OK, it's 2/3 (5) and 1/3 (4)
How'd you get (5) and (4)?
Yes?
AUDIENCE: When you have that [INAUDIBLE] nodes, you may
know that there's [? a procedure ?]
[INAUDIBLE] before.
PROFESSOR: Good.
If you're walking around Manhattan, and you're trying
to go from A to I, and if you spun around in this loop and
came back to A, that would probably be a bad path, right?
So revisiting a place you've been before is probably a bad
idea, if what your goal was, was to get from A to I in the
shortest possible distance.
So that's exactly right.
So I would like to identify instances where I go back to
where I started.
So for example, that A. That A is bad.
That means I went back to the start place.
I'm just starting over.
So if I think about those, I can identify by red all the
places where I'm repeating.
So ABA, don't really care what happens after that.
ABCB, well, that's B again.
So that's just brain dead, right?
So I can actually remove a fair amount of the tree by
simply getting rid of silliness.
Don't start the path over again, where "over" means if
you come to a place you've been before,
stop looking there.
That's not the right answer.
And so you can see there that I actually
deleted half of the tree.
The number of nodes on the third line was 16.
And 8 of them had the property that they repeated.
Yes?
AUDIENCE: [INAUDIBLE]
[? after D. ?]
Are you [INAUDIBLE]?
PROFESSOR: This B and D. So there's no reason to consider
this D, even though the D didn't repeat.
AUDIENCE: That would be [? AD? ?]
PROFESSOR: ABC,
AUDIENCE: [? That would be E? ?]
PROFESSOR: ABED.
AUDIENCE: [INAUDIBLE]
PROFESSOR: So I didn't, in this path, ever hit D before.
AUDIENCE: When it did lead to the path there, you'll get the
same one, AD [INAUDIBLE]
PROFESSOR: I guess I don't understand.
AUDIENCE: What I'm saying is that without getting the path
AD, I still get (2)?
PROFESSOR: Yes.
So this D seems clearly inferior to that D. Yes,
that's absolutely true.
So this is a very simple rule for removing things.
You're thinking of a more advanced rule.
So if you saw--
if there's a shorter path to a particular place, don't look
at the longer path.
You're absolutely right.
So in fact, there might be more severe pruning
that you could do.
There might have been an answer that was
bigger than 8 --
right?
And so you're absolutely right.
Ok.
Let me ignore that for the moment and come back to it in
about four slides.
You're absolutely right.
So what we want to do now is take that idea of throwing
away silly paths and formalize it so that we can put it into
the algorithm.
And we'll think about that as pruning rules.
So the first pruning rule is the easy one.
Don't consider any path that visits the same state twice.
That doesn't pick up your case, but it does pick up 8
cases here.
So that's easy to implement.
All we need to do is--
down here where we're thinking about whether this is a good
state to add or not--
we just ask, is it in the path?
So if the state that I'm about to put in the path is already
in the path, don't put it there.
If you don't shove it back into the
agenda, it'll get forgotten.
So before you shove it into the agenda, ask yourself the
question, is it already in the path?
And so I do that here.
Keep in mind, I popped out an element called the parent.
I'm looking at the children.
The children's state is called "new state." So I ask, is new
state in the parent's path?
So parent.inpath of new state.
So that means I have to write inpath.
Inpath is easy.
It's especially easy if we use recursion.
So inpath says, if my state is state, then return True.
I'm in the path.
If that's not true, and I don't have a parent, then that
means I'm the start state.
That means it wasn't in the path.
And if neither of those is true, ask the same
question of my parent.
So that makes it recursive.
So consider two cases that could either
make it true or false.
It would be True if I'm currently sitting on a node
that happens to be the same state.
It would be False if I recursed the whole way back to
the start state and hadn't found it yet.
So there are two termination states--
I landed on a state in the path that was the same as new
state, or I ran the whole way back to the start state and
didn't find it.
Those two terminate by doing returns --
return True or return False.
The other option is that I don't know the
answer, ask my parent.
So just recurse on inpath, and ask my parent
to do the same thing.
So that's the way I can figure out--
I can implement pruning rule (1).
Now pruning rule (2) --
if multiple actions lead to the same state, only think
about one of them.
That actually doesn't happen on the Manhattan grid problem.
Because you can imagine search cases where there are three
different things that you could do.
In fact, you saw some of those when you were coding
the robot last week.
There were multiple ways you could end up at the state at
the end of the hall.
You could get there by being there and moving left, which
you hit the wall.
Or you could get there by being here and moving left.
Both of them left you in the same place.
So if you're planning a search, you don't need to
distinguish among those, because they take the same
amount of steps.
So since they take the same amount of steps, we don't need
to search further.
So we can collapse them.
That's called pruning rule (2).
That's also easy to implement.
What we do is, we keep track of, for every parent, what are
all of its children.
If the parent already has a child at that place, throw
away the excess children.
That doesn't sound good.
So keep track of how many child states I have.
Make a list.
And if the new state didn't satisfy the goal, ask if it's
already in the list of children.
If it's already there, pass.
Don't do anything.
Otherwise, do pruning rule (1).
And then, before you push it into the agenda, also push it
into the list of new children.
That's a way of making sure that if there's multiple ways
to get the same state, you only keep track of one.
So that's an additional pruning rule.
So now let's think about how we would implement these.
Let's think about the solution to a problem where we want to
apply a depth-first search on this Manhattan problem, to get
from A to I. So let's think about--
let's go up--
so I want to think about, how do I apply depth-first search
to that problem?
So think about the agenda.
So the agenda, I initialized it with the node that
corresponds to the start state, so that's A. I'm doing
depth first.
What's the rule for depth first?
Pop the last guy, replace it by his children.
OK, so pop the last guy.
What's the last guy?
The last guy is A. Replace it by his children.
What's his children of A?
Well, there's two of them, AB and AD.
Ok.
So I'm done with the loop for the first level.
So pop the last guy, that's AD.
Replace it by his children.
What are the children of AD?
Well, what could D do?
D could go to A or E or G. A's brain dead, so I
don't want that one.
So I'll think about E and G.
So ADE, ADG.
By the way, stop me if I make a mistake.
It's really embarrassing.
OK, pop the end, ADG.
Who's the possible children of ADG?
ADG?
Well, it could go back to D, but that's stupid.
So ADGH seems to be the only good one.
Pop the last one, ADGH.
And who's his children, ADGH?
ADGH has children E, G, and I. But I don't want G, because
that's brain dead.
So ADGH, E or I. And that one won, right?
Because I got to A.
Everyone see what I did?
I tried to work out the algorithm manually.
So the idea, then, was that--
so how much work I do?
I visited 1, 2, 3, 4, 5, 6, 7.
And then I found it.
So I did 7 visits.
And I got the right answer.
So both of those are good--
7 is a small number, and getting the right answer.
Both of those are good things.
And in general, if you think about the way a depth-first
search works--
here's a transcript of what I just did.
This will be posted on the online version.
So you can see it, even though it's not
handed out to you now.
So you can look this up on the web.
So in general, depth-first search won't
work for every problem.
It happened to work for this problem.
In fact, it happened to be very good for this problem.
But it won't work for every problem because
it could get stuck.
It could run forever in a problem with infinite domain.
This problem has infinite domain.
So if I were to choose my start and end state
judiciously, I could get it stuck in an infinite loop.
That's a property of depth-first search.
Even when it finds a path, it doesn't necessarily find the
shortest path.
Well, that's a bummer.
But it's very efficient in its use of memory.
So it's not a completely brain-dead search strategy,
but it's usually brain dead.
So let's think about breadth-first search as an
alternative.
Again, all we need do is switch the idea of thinking
about stacks versus queues.
Take off the beginning, add to the end.
That's the way queues work.
So now let's do the same problem with a
breadth-first search.
So I start with the agenda.
I put in A. I pop off the head of the queue and stuff the
children at the end.
I pop off the beginning and stuff the children AB, AD.
That looks right.
That's the end of path one.
Now I pop off the beginning and stick in the children.
What are the children of AB?
Well, AB could go to ACE.
A is brain dead, so ABC--
ABC or ABE.
ABC or ABE, that looks right.
Now pop off this guy, AD, and put his children at the end.
That's ADE and ADG.
I don't think I made a mistake yet.
Pop off the first guy, ABC.
Stick in his children ABC--
ABC, it could go to B or F. Looks like F is the only one
that makes any sense.
ABC, that looks right.
ABE, put his children ABE--
E could go to B-- that's brain dead--
D, F, or H. D, F-- wait.
AUDIENCE: AB.
PROFESSOR: AB, thank you.
I'm supposed to be doing ABE followed by something, ABE
followed by something.
I don't want B. D is fine, F is fine, and H is fine.
D, F, and H. OK so far?
AUDIENCE: [INAUDIBLE]
PROFESSOR: Oh no, it's not right?
OK, what did I do wrong?
AUDIENCE: Just AB.
It's the fourth--
PROFESSOR: Oh, here.
That's up here.
Is that [INAUDIBLE].
AUDIENCE: Yeah.
PROFESSOR: Thank you.
That would be embarrassing.
OK, next pop off AD--
This is why we have computers, right?
We don't normally do this by hand.
OK, so ADE--
if I had ADE, I could do B--
that seems OK, D seems bad--
F, or H. So it would look like
B, F, H. OK, ADG.
A, D, G. It looks like H is my only option.
ABCF.
A, B, C, F. Looks like I could do E or I. Finally.
Now the only question is whether I got the
right number of states.
Let's assume I did.
So 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 --
which happens to be the right answer.
At least it happens to be the answer I got this morning when
I was at breakfast.
So what did I just do?
I just did a breadth-first search.
Here's a transcript.
16 matches, good.
Breadth-first search has a different set of properties.
Notice that it took me longer.
But because it's breadth first, and because each row
corresponds to an increasing path length, it's always
guaranteed to give you the shortest answer.
That's good.
So it always gives you the shortest answer.
It requires more space.
I mean, you can see that just on the chalkboard.
And also it still didn't take care of your problem.
So this still seems like there's too much work.
I looked at 16 different places.
I did 16 visits.
There's just something completely wrong about that,
because there's only 9 states.
How could it take more visits than there are states?
So that just doesn't sound right.
And it's for exactly your point.
And so there's another idea that we can use, which is
called "dynamic programming." The idea in dynamic
programming, the principal is, if you think about a path that
goes from x is z through y, the best path from x to z
through y is the sum of two paths--
the best path from x to y and the best path from y to z.
If you think about that, that has to be the case.
And if we further assume that we're going to do breadth
first, then the first time that we see a state is the
best way to get there.
So what we can do then, in order to take care of your
case, is keep track of the states we've already visited.
If we've already visited a state, it appears earlier in
the tree, there's no point in thinking about it further.
That's the idea of dynamic programming.
And that's also easy to implement.
All we need to do is keep track of all those places
we've already visited.
So we make a dictionary called "visited." So I initialize
before I start looking at the children.
I initialize right after I set up the initial
contents of the agenda.
I create this dictionary called visited.
And every time I visit a new state, I put that state in the
visit list.
Then before I add the child to the agenda I ask, is the child
already in the visit list?
If the child's already there, well forget it.
I don't need him.
Otherwise, just before you push the new state, remember
now that that's an element that's been visited.
So the idea, then, is that by keeping track of who you've
already looked at, you can avoid looking--
so if there's a depth-3 way to get to D, and a depth-2, then
I don't need to worry about the previous ones, because
it's already in the visit list.
Yes?
AUDIENCE: Why do we still need the new child states up there?
PROFESSOR: Why do we still have the new child states?
AUDIENCE: The placement was based [? on-- ?] the
[INAUDIBLE] state.
PROFESSOR: I think you're right.
I should think about that.
I think you're right.
I think when I was modifying the code for the different
places I slipped and could have removed that line.
I think you're right.
I'll have to think about it, but I think you're right.
So if that line magically disappears from the online
version, he's right.
OK, so now one last problem.
I want to see if I can figure out what would happen with
dynamic programming.
So I want to do breadth first.
And just as a warning, I'm hypoglycemic at this point.
So there may be more errors than usual.
So I need to keep track of two things.
I need to keep track of the visit list.
And I need to keep track of the agenda.
So there's two lists I have to keep track of.
OK.
Let's start out by saying that the agenda
contains the start element.
That's A. That means we visited A. It's breadth first,
so I want to take the first guy out of the queue and add
his children to the end of the queue.
So take the first guy out of the queue.
Add his children.
A's children are B and D, which means that I've now
visited B and D.
Now I want to take out the first guy from the queue, AB,
and I want to put his children at the end of the queue.
AB's children are A--
that's been visited, C--
not visited, and E--
not visited.
So ABCE.
But that visits C and E.
Now I want to take out AD and put its children at the end.
AD is AEG.
A is visited, E is visited, which leaves just G, so ADG .
And that visits G.
Then I want to take out ABC and put in its children, A, B,
C. ABC, oh dear.
AB--
I'm looking up there.
I said I'm hypoglycemic.
ABC--
ABC -- could be B or F. Well, B's no good.
Which leaves F, but that visits F.
So now, ABE.
ABE, so that could be B, D, F, H. B--
visited, D--
visited, F--
visited, H--
OK.
That visits H.
Now take out ADG.
Children of ADG--
ADG, two children, D and H. D and H, they're both there.
That didn't work.
There are no children of ADG.
ABCF--
ABCF, three children--
C, E, I. C--
visited, E--
visited, I--
done.
Found the right answer.
1, 2, 3, 4, 5, 6, 7, 8 --
8 visits.
So I've got the same answer, and it's optimal, and I did
fewer than 9 visits.
9 was the number of states.
So this algorithm will always have those properties.
So the dynamic programming--
oh, I forgot a slide.
The dynamic programming with breadth-first search will
always find the best.
It will never take longer than the number of states.
So in this problem that had a finite number of states, even
though I had an infinite number of paths--
because you can go around in circles--
it'll still never take more than the number of states.
And all that it requires to implement is to maintain two
lists instead of one.
So the point, then, is that today we looked at a variety--
we looked at two real different kinds of search
algorithms, depth-first search and breadth-first search.
And we looked at a number of different pruning rules.
Pruning rule (1) -- don't go to some place that you've
already visited.
Pruning rule (2) -- if you have two children that go to
the same place, only think about one of them.
You can consider dynamic programming to be a third
pruning rule, because that's the effect of it.
And the final announcement, don't forget about Wednesday.
Have a good week.