[SQUEAKING] [RUSTLING] [CLICKING]
JUSTIN SOLOMON: Right, so today is going to be our first,
I believe, of two problem sessions
covering dynamic programming.
I've learned dynamic programming is
one of these interesting parts of an algorithms class
where, somehow, the people that are really good at it
are completely disjoint with the people that
are good at all the other parts of the algorithms class.
So for some of you guys, that might be promising,
and for others, maybe a little bit less so.
So maybe we'll spend just a minute or two reviewing
the basic ideas we're going to apply in these problems
because they'll follow, more or less, the same template.
Although, of course, as usual in 6.006,
we like to put some interesting window dressing around it
so that it's not totally obvious what you're doing.
And then we'll do a bunch of sample problems.
Right.
So let's talk a little bit about dynamic programming
and the basic idea here.
So, dynamic programming is kind of a funny outlier in 6.006
in that--
for example, in the data structures part of the course,
we learned, like, what--
now I'm struggling to think of a data structure--
like a useful-- like trees and arrays or whatever.
And these are actually things that you can code.
if you look and see if there are--
well, plausibly, it could be an implementation
of a tree in there somewhere.
And so these are useful algorithms
that you can maybe even read the pseudocode.
And there's a universe where you really
do translate that pseudocode into something
inside of your laptop.
Dynamic programming is a little bit less so.
This is more of a meta--
I don't know if you'd call it a meta algorithm
or problem solving approach or what,
but it's not like you somehow say,
I'm going to apply the dynamic programming
algorithm to this problem.
But rather, it's sort of this big class of things that all
follow a similar template or sort of approach to thinking
about problem solving, which I think sort of explains why,
actually, in some sense, the last couple of lectures that
you've seen-- and, I guess, if I'm getting the time sequence
of our course right, the next couple of years that you will
see--
and the problem sessions actually
start to coincide in the sense that when Erik was teaching
you guys dynamic programming, how did he do it?
Well, he didn't write down-- well,
he sort of wrote down some template
for dynamic programming, but then we just did
a bunch of sample problems.
And that's exactly what we're going to do today.
So, somehow, all of these things are just
going to converge in this part of our course
because dynamic programming, it's
really more of a way of life than any particular algorithm.
And this is a pattern that I think
you see a lot in advanced algorithms.
Like, for example, in my universe,
in numerical analysis, when you talk about the ADMM algorithm,
it's actually a totally useless algorithm.
What matters is applying it to a particular problem.
And this is sort of, I think, a more mature or grown up
way to think about a lot of things
in algorithms, that pretty soon, this sort of general purpose
stuff that's useful all the time,
I think, it starts to disperse a little bit in favor
of different patterns and mechanisms
that you're used to thinking about.
So there's my 10-second, sort of,
philosophical introduction to what we're doing,
during which I've managed to chase
this table across the room.
You know, I played--
I did college on the west coast, and I
thought I was going to be a music major.
And there was a piano master class where
we forgot to put the little clips on the wheels
and there was an earthquake, and I just
thought I was really nervous because the piano was literally
slipping away from me.
I can never think of that Chopin nocturne in quite the same way.
But in any event, in dynamic programming,
Erik laid out for you guys a particular, sort of,
set of steps that are a useful problem-solving approach
in the dynamic programming universe.
In today's problem session, I'm going
to try and help you guys translate a little bit
from this template to what it means to actually write code
to implement a dynamic programming algorithm
because I think it's a little easy to forget that here.
But, on the other hand, on your homework,
when you're writing out answers to algorithms problem,
it's perfectly fine to follow this template even letter--
I guess-- literally letter for letter and answer each
of these questions.
And then the remaining glue that you need to actually write
the code is not terribly exciting
from an algorithms theory perspective.
So the basic idea here is that there's
a lot of different problems that can be written recursively,
in some sense.
Certainly, we've encountered many of those in this course.
In fact, I think the bias in the way
that we've presented algorithms that
don't have to be recursive is to write them in a recursive way.
And the point here is that when you have a recursive call
and you repeat something, you give the function
the same input more than one time,
you might as well remember what you got the last time you saw
that input, and then you don't have
to do that computation again.
Really, in one sentence, I think that's roughly the logic behind
all these dynamic programming things.
So there's no reason to be too redundant with lecture.
For just the 10-second overview, I
think that there's an example which is simultaneously
good and misleading, which is this Fibonacci sequence.
It's good in the sense that the logic of dynamic programming
is really easy.
It's bad in that the runtime is kind of weird to think about.
But remember, though, your Fibonacci sequence
looks something like f of k equals f of k minus 1
plus f of k minus 2.
And if you look at your, sort of, recursive call tree here--
like, let's say that I do k equals 4.
Then it's going to call--
my function f is going to have to evaluate it at 3 and 2.
And then the 3 is going to evaluate at 2 and 1, and so on.
And the thing to notice is that when I call f of 4--
or, rather, f of 3 here, if there were 3 somewhere else
in my tree, I get the same number, so,
in particular, f of 2 and f of 2.
Both of these are going to take some amount
of algorithmic work, but if just the first time
I see a 2 I have a little piece of scratch paper
and I say, oh, any time I see k equals
2, just return this number rather
than doing recursive calls, then, in effect,
if there's any subtree underneath this thing,
I've just pruned it from my tree.
And so that's the basic logic here.
And that's basically the paradigm
that's going on in this SRTBOT acronym, which
is you first take your problem and divide it into subproblems.
That is mysterious.
Why is this board moving?
Oh, there's a phone in my pocket and I bumped against the wall.
I'm not used to this classroom.
Right.
Yeah.
So the first thing you want to do
is to write my problem as this sort of form.
Notice just that we've done this a lot in this class,
we've written things recursively.
The difference here is the sort of argument
that goes into recursion is typically, maybe,
a little simpler than putting some giant data structure
inside of there or something like that.
So, for instance, merge sort, you
could write in this paradigm.
I guess we covered that, but it's probably
not the most natural way to think about merge sort.
Then we need to relate our subproblems to each other.
So, for instance, in the Fibonacci sequence problem,
I just gave you the relation that--
what defines the problem.
Incidentally, this is, what, a model for the reproduction
of rabbits, I think, if I remember
reading the history of the Fibonacci sequence.
And then, I think, to me, the most--
not necessarily unnatural-- but I think the thing that maybe is
hardest to translate to an algorithm if you're thinking
about writing code is this--
oh, man, this is going to be a problem--
this idea of topological order.
The basic idea here is that if f of 1 depended on f of 2,
and f of 2 depended on f of 1, I'd be in a lot of trouble,
right, because somehow my tree would never
converge, for one thing, if I made these recursive calls
and I'd never be able to memoize or, kind of,
remember a value when I move on.
Right?
And so the idea here is that there's
some ordering of my subproblem so
that I can build up a solution.
And there's, sort of, two dual ways
to think about why that's useful.
So in the memoization universe, what do I do?
I just add an if statement saying if I've already
evaluated f of k, return it.
That's perfectly fine.
The other thing I can do is if I write my problems
in topological order, then I can sort of
go in the reverse direction and build up my memoization table.
So, for instance, for the Fibonacci sequence problem,
I could do f of 1 and then f of 2 and then f of 3 and f of 4
all the way until I get to the k value that I actually wanted.
And those are just duals of the same coin.
They're exactly the same approach.
Although the memoization version,
sometimes you can prune out subproblems
that you didn't actually need to solve.
So, for instance, maybe this was f of k minus 7,
and so I can skip a few indices in my array.
I don't think, typically, that has a big effect on runtime
for the problems that we've seen,
but it could, plausibly, in some universal.
I'd have to think about a problem
where that makes a difference.
Right.
And then I think the BOT part of SRTBOT
is a little easier to think about.
You have to make sure that this recursion has a base case,
like when is this thing going to stop.
That's exactly the same.
It's just any recursive algorithm.
The O for original, I think, is a little bit
retrofit to make SRTBOT sound nice,
but I think the idea here is that you
need to go back to your original problem
and make sure that it corresponds
to one of the function calls that you've written
in all this complicated stuff.
Hopefully that's a reasonable characterization.
And then, finally, the t is more--
these are for describing your algorithm.
The last one is for analyzing it.
And, again, the BOT part of SRTBOT
almost applies to anything we've done in 6.006, like you should
always analyze your run time.
OK.
So, in any event, that's my 10-minute version
of the last couple lectures and, I think, more or less,
enough to get us started with some sample problems here.
Sorry, I couldn't help it.
I like to teach things.
OK.
So, right.
So in our problem session, we have
a few of the homework problems from last year to go over.
If it makes you guys feel any better,
I got myself all balled up on one of them
last night while I was preparing for today.
And I look forward to doing that in front of all of you guys
now.
Right.
So, I'm afraid of this, so I'm going to go to the next board.
OK.
So in our first problem, Sunny studies--
this was-- somehow, the cute naming
conventions we have in 6.006 got really meta in this problem,
because there's a problem about Tim the Beaver.
But, as we all know, Tim is MIT backwards,
so he happens to fit into this goofy game
that Jason likes to play in writing homework problems.
Anyway, but it's also the MIT mascot.
Anyway, I got very excited.
Right.
So what's going on in this problem?
So Tim the Beaver has kind of an interesting-- you know,
mathematics, I think you would call this a martingale if you
flip the coin a little bit when he solve this problem.
But luckily, Tim the Beaver is a deterministic kind of a guy.
And he looks at the weather outside,
and if it's a temperature t-- apparently,
Tim the Beaver is OK with boiling.
The higher the temperature, the happier Tim gets.
So this is a first derivative kind of a phenomenon.
In particular, on a given day, if I have a temperature t,
Tim the Beaver has two things that he
can do to change his mood.
Apparently, Tim the Beaver's mood never stays fixed.
It always goes up and down.
In particular, he can either go outside, in which case
the happiness increases by t, OK, or he can stay inside,
in which case his happiness decreases by t.
OK.
So every day, Tim the Beaver, he wakes up he--
I really want to say that he checks for his shadow,
but that's a gopher, right?
i any event, he wakes up in the morning, he checks the weather,
and he makes the determination does
he want to go outside or not.
And if he goes outside, he gets happier
by an amount that's equal to the temperature.
If he stays inside, he gets less happy by an amount that's
equal to the temperature.
By the way, I think our solution is perfectly fine
if temperatures are negative here, in which case,
I guess, everything would flip intuitively.
But there's no reason to get too hung up on that.
But, of course, there's a twist here.
So Tim, as with many of you, has n days until his final exam.
And he's worried about studying.
Yeah?
So, in particular, he never wants to go--
he's come up with a personal resolve
to never go outside more than two days in a row.
Yeah?
So, right.
And so the question is-- right, because that way
he has to stay inside and study at least one
out of every three-ish days.
OK.
So the question is how can Tim maximize his happiness.
Incidentally, in machine learning,
sometimes they call that minimizing regret,
which I always found to be a very sad way
to think about algorithms when there's a totally dual version.
But Tim's an optimistic guy.
He wants to maximize his happiness subject to this
constraint that he cannot go outside more than two days
in a row.
Right?
So if I go out on Monday and Tuesday,
I have to stay inside on Wednesday.
Yes?
AUDIENCE: I think there's no effect to his happiness
when he stays in.
JUSTIN SOLOMON: There's no effect to his happiness
when he stays in.
AUDIENCE: At least, that's [INAUDIBLE]..
JUSTIN SOLOMON: No, it says with a decrease in happiness
when t-- oh, when t is negative.
That's not actually going to affect our problem at all.
AUDIENCE: It's not going to affect it.
JUSTIN SOLOMON: Sure, yeah, I can fix this live.
This is what happens when I do the problem myself
before looking at the answer and then don't check it closely.
Fine.
So let's change that.
I like this problem better, somehow, psychologically.
But that's OK.
Right.
So, Jason correctly points out that if you actually
read the problem, what's asked there is slightly different,
that when he goes outside, his happiness increases by t.
If he stays inside, his happiness does nothing.
Right?
So it stays the same.
My apologies, so Tim the Beaver is a particularly optimistic
beaver.
His happiness can only increase in time,
assuming he lives in a climate with positive temperatures.
OK.
I think I've got it right now.
Cool.
We'll see if I can still do this.
Yeah, I think basically nothing changes.
OK, that's great.
All right.
We're going to do it.
OK.
Right, so the question is how do we solve this problem.
And thankfully, I think we put the easiest problem first.
And, in particular, if we're following our SRTBOT paradigm
here, somehow there's a set of subproblems that
are staring us in the face.
That's the word I'm looking for.
In particular, well, there's sort
of only one index in our problem, which
is what day it is.
So the obvious thing to do would be to say,
can we figure out the maximum amount
of happiness for days, say, i to the last day?
By the way, if I do that, I'm using the prefix version
of my problem--
ah, suffix version of my problem.
I could also do it the opposite way and work from the end
back in.
Maybe if we have time all the way at end,
we'll do the second one.
But it doesn't really matter.
OK.
So, in particular, just to add a little bit of notation,
let's say that t of i is equal to the temperature on day i.
OK.
And now we're going to make a new thing, which
is going to be the actual variable we want to compute.
This is going to be x to i, which, we'll write,
is the maximum happiness that you can achieve if you only
consider the calendar from day i to day n, I guess inclusive.
OK.
Incidentally, just for convenience,
we'll assume that x i is equal to 0
if I go past the end of my array, which I think
is, kind of, a, typical thing to do in these type DP algorithms.
OK.
So the question is can we actually come up
with a recursive algorithm that computes
x i using this nice, sort of, typologically acyclic way
of thinking about our problem.
The answer is obviously yes, or I wouldn't be here today.
And so in the absence of a smarter idea,
let's just do the Toucan Sam approach here and follow
our nose and see if we can just write our problem in terms
of other ones.
So, in general, let's say that Tim the Beaver
wakes up on day i.
He has, basically, two decisions that he can make, right?
He can either stay inside or he can not stay inside.
He can go outside, right?
So let's just basically handle these three cases.
So in case, one he stays inside.
Well, now what happens to his happiness?
Well, according to my revised version
of this problem, nothing, so, in particular, what do we know?
Well, if he stays inside, then he has--
any decision he can make tomorrow, it doesn't matter.
He can go inside, he can go outside,
whatever, because by having stayed inside,
he's earned himself two free days of going outside
if he wants.
Right?
So, in particular, in this case, we can convince ourselves
that this is true, I think.
Yes, so, in other words, while he gets no utility for today,
he wakes up tomorrow and he can make whatever decision
he wants.
OK.
The second thing he can do is go out.
This is where things get a little tricky.
Right?
Can I just do, like, take t i and add it to x i plus 1?
What goes wrong?
AUDIENCE: You go out three days--
JUSTIN SOLOMON: Maybe you go out three days in a row, right?
Somehow, you have to remember that, right?
And so that's where things are a little bit of a headache,
that, in particular, if I go out today and tomorrow,
I can't go out the day after that.
And somehow, if we just dealt with this one case
as t i plus x i plus 1, we wouldn't remember that.
And that's a problem.
So, instead, what we can do is think of there
being two subcases, right?
So what we're going to assume is that not only does he
go out today, but that he's free to go out tomorrow.
And we're going to make that recursive assumption
as we move down our array.
So if we do that, well, now we have case a and case b.
So in case a, he goes out today and he stays in tomorrow.
Yeah.
OK.
So what happens in this case, well--
by the way, I'm using this kind of weird arrow notation.
I don't know if this is good or not,
but, essentially, the point is that I'm
keeping track of cases, and then eventually I'm
going to want to have to take the max
overall of these things.
So I don't like the equal sign because somehow that's
a little misleading.
Right.
So in that case, well, he gets the utility
of having gone out today.
Tomorrow he stays in, which means
that the day after tomorrow he can do whatever the heck
he wants.
He has free reign.
So I can write that using this recursive call.
OK.
Similarly-- right.
I'm getting the hang of this.
Sorry, this is way too entertaining for me.
I can play with this board all day.
OK.
So in case 2b, he goes out today and he goes out tomorrow.
OK?
So--
AUDIENCE: He's a party animal.
JUSTIN SOLOMON: He's a party animal.
He is an animal and he's going out a lot.
Right, so in that case, what happens?
Well, he gets that.
He gets today's utility.
He gets tomorrow's utility.
The day after, he has to stay in,
so we might as well skip it.
And then he can do whatever he wants the day after that.
OK?
So if we go back, I guess, technically, we
should revise our definition of x a tiny bit,
that it's not the maximum of happiness-- well,
we can convince ourselves that it's the same thing.
But really, it's not the maximum happiness for day i through n.
It's the maximum happiness for day
i through n under the assumption that he has
permission to go out on day i.
Right?
And that's really what's going on
in our recursive set of calls here.
OK.
So does our recursion make sense here?
Cool.
All right.
So let's see here.
So if we're following our SRTBOT--
I keep reviewing papers that use the word paradigm a lot,
so I feel like I should do that.
So what is t?
It's the topological order.
Notice that x i only depends on larger i's.
So in terms of our topological order,
the dependence graph is really simple.
It's just a line, so remember that you
can think about topological order
or you can think about being acyclic graph.
Those are equivalent.
We covered that in this course.
I kind of like thinking about acyclic graphs.
So x1 depends on x2 depends on x3 depends on x4.
That graph has no cycles, so we're good.
Right.
So next we have to come up with our base
case for our recursion.
Notice that the way I have chosen to solve this problem
is by calling future indices, which
means that my base case sits at the end of my array
because that's sort of like the lowest down on the recursion
train.
The recursion chain is what I was going for,
but I kind of like the recursion train better.
In particular, on day n--
well, if he has permission to go out on day n,
he can do one of two things.
He can either go out or not.
It doesn't matter, right?
So, in particular, we can say that that's the max of 0
or t of n.
Remember, I didn't tell you that temperatures
have to be positive.
Maybe he's a Celsius kind of a beaver.
OK.
Right.
And then in addition to the--
for convenience, notice that, like,
there's a universe where I look beyond the end of my array
in my recursive call here, so I should probably think about
a few extra x's.
Obviously, the utility of going out on a day that doesn't exist
is 0.
So we can say that x n plus 1 equals x n plus 2 equals 0.
OK.
I've managed to use way too much space for one
simple algorithms problem.
OK.
Yeah?
I get credit for that?
OK.
Right.
So now we need to do the o and the t.
So what's our original problem?
Well, remember that he wants to maximize his happiness starting
on day one, so our original problem is just x of 1,
or is it?
So, remember that Tim the Beaver--
your instructor is very sloppy when
it comes to actually reading the problems,
as you saw at the beginning.
A second mistake for which I would have personally
lost points were I to solve this problem on my homework
is that it didn't ask for just the maximum amount of happiness
that Tim could achieve--
that's not very practical for your everyday beaver--
but rather, he wants to know the actual plan.
He wants to know what days he can go out and what days he
can't.
Yeah?
And I haven't actually told you how to do that, right?
I've only told you how to compute
x, which is just the maximum amount of happiness.
If I were you guys, I think this is
a perfectly reasonable simplification that's
like a warm up problem to solve.
In fact, I would argue it's less of a warm up and more
the crux of the problem--
and then going back and making sure
you can convince yourself that you could actually
reconstruct the solution.
My way of solving this was ever so slightly
different from the one in the problem,
but they're equivalent, which is to say
I can make a second array--
I won't write it down, because I'm slow at writing--
that just says on every day, whether I took option 1, option
2a, or option 2b.
And now I can reconstruct my plan very easily, right?
So I look at x1, if I took option 1, then I stay in
and I look at day two.
If I took option 2a, then I can label today, tomorrow,
and the day after.
Oh, wait-- yeah, that's right.
I can label today's choice, tomorrow's choice, the day's
after choice, and then look three days later
and recurse that way.
Option b is kind of similar.
So a reasonable way to reconstruct the actual set
of what days you go out and what days you go in
is just to remember, as you do your memoization or whatever,
whether you did option 1, 2a, or 2b.
And then it's pretty easy to reconstruct from there.
Maybe I'll let you guys convince yourselves
of that at home or in the last 8 seconds
if you happen to be the two audience members that I have.
And then, finally, we need to do our time thing.
And most of the time arguments here
follow more or less the same pattern,
which is you count the number of subproblems
and the time per subproblem, you multiply those two things
together, and you get your runtime.
We're going to see in one problem
on this problem set that's not quite right because we
have to account for some precomputation.
But in this case, it is.
Right, so let's see, what are our subproblems here?
Well, essentially-- I guess I didn't actually say it,
but you have to take the max of these three values.
This is the max of three expressions which
have a constant number of plus signs and lookups
and memory and all that good stuff.
So each subproblem takes order one time.
How many problems are there?
Well, there's, I guess, n plus 2 max,
if you want to be conservative about it.
So, in particular, there's order n subproblems, right?
So all I have to do is multiply these two things together,
and my algorithm takes order n time.
And that's our solution to problem number one.
Any questions so far?
Yes?
Uh-oh.
AUDIENCE: When I was thinking about the problem beforehand,
I was wondering could you use base cases--
right now, we have two different kinds of base case,
a base case for x of n and a base case for things after.
Can I remove the first one and add an x of n
plus 3 equals 0 as well?
What would that do?
JUSTIN SOLOMON: Could I remove the first one
and add an x of n plus 3?
Yeah, I guess that's fine.
I'm sorry, that's not a particularly helpful answer
for the people watching on video.
My answer to this question you can't hear is yes.
So the question, to repeat, was this base case was somehow
kind of complicated-looking.
To be fair, it's the one that I was
given in [INAUDIBLE] assignment, but that's OK.
But the question was is this truly necessary.
In particular, can I get rid of the x n case
and instead add a third day past the end of time,
which also has value 0?
And if you, kind of, look at that plus case b, I think--
or rather, case a--
think you can convince-- well, case a and b,
for that matter-- you can convince yourselves that these
are equivalent, right?
That's absolutely right.
So I could add a third day after the end of this thing, which
also has value 0.
Or, by the way, I could just say in my code if n is--
if i is bigger than n, return 0.
That's the same thing.
Yeah, and then I guess I don't have
to worry about that x n case.
Yeah, these are the same.
To each their own.
Fabulous question.
Any others that I can answer while we're at it?
Cool.
All right.
So that's problem one.
Writing too big-- I don't like this big chalk, you know.
OK.
So problem two is the one that got me all hot
and bothered yesterday.
So let's see if we do any better in front of people,
because that's usually the best way to improve
a problem solving skill.
Right.
So in problem two, which, annoyingly,
is also probably the most practical problem
on this problem set.
Essentially, you have a--
I suppose I should write some stuff down.
So in problem--
I used the wrong side--
two, you have an operating system Menix--
whatever-- which is--
apparently, it's very simple.
AUDIENCE: Menix, Unix.
JUSTIN SOLOMON: Oh, I get it.
[LAUGHS] It doesn't mean I have to like it.
Right.
[LAUGHS] So, in Menix, apparently,
the only thing that my operating system can do
is compute edit distance between files.
And it wants to do so efficiently.
So we have that a file is a sequence of strings.
And I believe we say their length is
less than or equal to k.
That's going to come into play a little bit later.
And the strings are basically just lines
of the different files.
So there's three different ways that we can change a file.
So here are the changes we can do.
Change number 1 is to add a line.
Change number 2 is to remove a line.
And change number 3 is to swap.
But a caveat for this interesting model
of what's cheap and what's not is that, apparently,
swapping two lines is cheap because they exist in memory.
Like, maybe I'm, I don't know, using a linked
list or something to store files,
and so swapping two pointers isn't so bad.
But inserting and removing a line
is hard because, I don't know, memory allocation is expensive,
like Menix is actually operating on clay tablets.
And I can chop my clay tablets into different slices
and just pick them up and swap them, and that's fine,
but if I want to add a line in my file,
I have to go to the Tigris and Euphrates and pull out the--
or whatever it was, the Eugris and the
Tiphrates-- and pull out a stone.
It's a lot of work to make a new line or to dispose.
So these are expensive and this is cheap.
And so the question that I'm trying to sa--
that I'm trying to say and that the problem is trying to ask
is that you are given files A and B with n lines each.
We want to know what the minimum number of non-swap operations
it'll take, and so, in other words,
the minimum number of time for you to add and remove lines
to transform A into B, essentially, with low cost.
And, in fact, just to be nice--
I think it's actually kind of a critical hint in this problem--
we give you the runtime.
And I'm going to, kind of, ignore it in my answer,
notice that I did something wrong, and then go back
and fix it.
This is different from the way the answer is written,
where god came in and said, like, oh, we observe that we're
probably going to need this thing,
so we're going to go ahead and do it here.
I think that's, maybe, not representative of the logic
here.
Right, so the runtime here is k n plus n squared.
The first thing to note is there is a k here.
Yeah, and so someday we're going to have to compare strings,
because that's what k is.
And I think that's the hint that's
implicit in this problem.
It's easy to miss.
And so, indeed, what we're going to notice
is we're going to look at our solution
and say, well, wait a second, if we didn't incur a factor of k,
we must have done something wrong.
And, indeed, that's going to be the case,
but it's only a minor fix to change it.
AUDIENCE: There's another important distinction
on this [INAUDIBLE].
JUSTIN SOLOMON: Oh, I'm sorry.
Yeah, right, when I swap things, they have to be adjacent.
I can't write at the bottom of the board.
That's supposed to be a, d, j, for those watching at home.
But they have to be-- you can only
swap lines that are adjacent, as they
appear in their original file.
I'll say it out loud rather than try and write it because you
it's going to take the rest of the lecture to do that.
OK, any other things I've forgotten?
There's a high likelihood.
I'm bad at this.
OK.
So this one was annoying.
And it's not actually annoying.
It's actually a relatively easy instance of a very well-known
dynamic program plus a tiny bit of additional stuff, which
is called edit distance.
In fact, I think if you guys are looking for intuition
on this problem, you might google
that one first as, sort of, a--
what was that?
AUDIENCE: [INAUDIBLE]
JUSTIN SOLOMON: Oh, in fact you're doing a recitation.
Oh, that's why it's not totally unreasonable to come
up with the answer here, even better.
But even if you hadn't, you know,
this is just another dynamic programming problem
that's just a little bit more annoying
than your average dynamic programming problem.
Now, the solution written out in the course notes
works from, sort of, the last line of the file
downward, in some sense--
upward, whatever.
I, like, literally lost two hours of my life
trying to think about editing files from the end up
and just getting myself all upset and confused.
So here, I'm going to attempt to do it in the other direction
and probably introduce a bunch of mistakes in the process.
So what do we do in dynamic programming
if we don't know what else to do?
We do sort stuff, SRTBOT.
And so let's do that here.
So, in particular, what are our subproblems?
This is a little bit funky.
So actually, even before we do the S of SRTBOT,
let's think about our problem a little bit.
Let's think about what it actually
means to edit a file because this
is what helped me think about the right answer
here, which is to say, you know--
so what's going on?
I have, like, two documents.
This is document A. This is document B. Each one of them
is composed of a bunch of lines.
And I'm basically trying to turn A into B.
And the only thing I can do is scroll out of line,
insert, just hit the Enter key, or do a third thing
where I kind of like swap two things that
are adjacent to one another.
That is the only thing I can do.
And the way I like to think about this problem--
there's kind of an annoyance here,
which I think is a typical annoyance
in dynamic programming problems, which
is that the order of operations suggests that this problem is
a lot, combinatorially, more difficult than it is, because,
like--
OK, let's think about how I actually edit documents-- like,
I spend 2/3 of my day editing bad grad student writing--
is like I'm jumping all over the place between different lines.
Like, first I delete this line, then
maybe I go to the bottom of my document
and delete some other one.
That would be a big problem from a dynamic programming
perspective.
I can't jump all over my document,
because keeping track of that whole edit history
is going to be somehow combinatorially ginormous.
Right?
I'm not the Track Changes button in Microsoft Word.
I want the minimum number of changes.
And if I have to recurse over all possible edits
to every single line in any order,
that's an awful lot of factorials
and 2 to the n's floating around that I don't want to have.
Right?
And so that's the sort of crux of the challenge here,
is to organize my approach to editing these files
in a way that doesn't require me to have
to do this sort of combinatorial jumping all over the place.
And I think it's also the one where there's a sort of--
like I know Jerry Caine at Stanford talks
a lot about the recursive leap of faith.
Like, somehow dividing your problem
into organized subproblems, that's
really where the challenge lives here.
So if I were a more organized PhD advisor,
the way that I would edit a file, or a clay tablet,
I guess, in this case, would be linearly,
that I might as well do whatever the heck
I'm going to do to line one before I move on the line two.
And at the end of the day, even if I did stuff
in a different order, you can convince yourself
that I could always order it in such a way
that all the edits that I do to the first line,
kind of, happen before lines later in the document,
with the possible exception of this swap thing.
But we'll see that somehow doesn't matter.
And, moreover, if I do an edit, I
might as well do the edit to make things better, right?
There's no reason to start willy-nilly inserting
and removing lines.
I might as well always do an operation that improves stuff.
And so thinking about that sort of logic leads me--
ta-da-- to a particular way that I might write down
my S, my some problems here, which
is to say that I'm going to think about editing my document
line by line.
So, in other words, once I've dealt with line one,
meaning that I found some way to mess with it
and make it match line one of the other guy,
I'm just going to think about removing it
and then think about the rest of the document.
You start saying, aha, that sentence sounds like recursion.
And that's right.
That's how we're going to solve this problem.
OK?
So, in particular, here's going to be our thing.
I'm going to do a slightly different one in the solution--
so you guys should all be vigilant--
which is I'm going to write x ij to be the min work to convert.
I'm not a Python programmer, but hopefully I got this right.
i colon is going to be everything from i
to the end of the file.
So in other words, this is the suffix version of our problem--
and into B j colon, like that.
OK.
So, in other words, I have a little--
it's kind of like a video-- like, think about Tetris.
Once you get that full line of blocks,
you can just throw that line of blocks away
and the whole video game moves down.
There's somehow something very similar
going on here, which is the second I've
managed to get a match for line one of document
into line one of the next document,
I'm just going to throw it away and pretend
like I have two documents with one less line in them.
Now, the thing that got me all hung up last night,
my original problem assumes that both of my documents
have the same length.
But here, I'm not making that assumption, right?
And, essentially, what we're going to figure out
is that that actually doesn't matter a whole lot,
that if I end up with one document of length k--
well, I shouldn't use k--
one document of length l and another document of length 0,
what's the amount of work that I should do to convert?
Well, l, because my only choice is to insert a bunch of lines
in one document, by the way, or delete a bunch of lines
from the other.
Those are dual to one another.
They're exactly the same.
I'm philosophizing a lot because I'm also convincing myself
that my answer is OK in the process.
OK.
So this is going to be our set subproblems.
And now we have to do the r, right?
We have to relate, something we struggle with in the math
department sometimes.
And, essentially, the way that I went about this
is to just do a billion different cases
of all the possible edits that I could do to line i and line j.
And that's perfectly fine in this problem.
I think the problem is a little slick.
And the way that they're written the solution,
they've convinced themselves that some things are
equivalent to others and removed them.
But you don't have to.
As long as there's a constant number
of cases, your golden Ponyboy.
So, in particular, let's think about some cases.
So first of all, if line i matches line j of my document--
remember that it's not really line j.
It's like making a document that just
happens to start at line j.
It's like taking scissors.
Well, then I can match them with zero-cost
because the beginnings are in the same place.
And I can move my Tetris came down one,
and that's perfectly fine.
So case one, I think, is the easiest one,
which is if A i equals B j, then I can just
remove that line from both documents
and move forward, in which case--
I'll use my same goofy notation--
I'm going to get that x ij.
Well, I'm going to just increment i and j
and keep going, like that.
Cool?
So what's something else I could do?
I could delete a line.
Yeah, so what happens--
OK.
So case two is delete A i, right?
That's a different thing I can do the line i.
Well, now what do I have to do?
I have a document on the left-hand side,
which is 1 line shorter.
And on the right-hand side, nothing changed.
But deleting a line cost me a dollar.
So, in particular, I have that x ij.
Well, what happens?
Well, I got rid of one line, but I had to pay.
OK.
Let's think about some other things.
You could delete B j.
This case actually isn't in the solution
because it turns out to be unnecessary.
AUDIENCE: Well, we're only allowed to edit A.
JUSTIN SOLOMON: Oh, I'm only allowed to edit A?
Oh, in that case, I don't have to delete B j.
I really didn't read these problems very closely.
That's my bad.
This would have made it much easier.
I really should read these things.
Cool, so that eliminates half of the cases on my notes.
Fabulous.
Incidentally, you could do these things on the other direction
and it really wouldn't change this problem a whole lot.
Sorry, you know, I have this bad habit
when I'm reading research papers of reading the research paper I
wanted to be there instead of the one
that's actually on the paper.
And, somehow, it's very similar phenomenon here.
OK.
Right.
So, great.
So I can only edit document A, which
makes this probably easier than what I was worried about.
Fabulous.
In that case-- ah, bananas.
With our third case here, well, let's see,
I could also insert a line.
Let's see.
So what ends up happening there?
So I can only edit document A?
So that makes my cases different than the ones
I wrote down on my notes.
Sorry.
OK.
So if I insert--
let's do this live.
Yeah, OK, so if I insert a line at line i,
I might as well make it match B j.
There's no reason not to.
Right?
I might as well kill off one element of B while I'm at it.
Yeah?
So if I do that, what ends up happening?
Well, I still have to match line i.
I've just, kind of, moved it lower in my file.
But I've, in essence, killed one line in file B
by making it match this new line that I inserted.
In my notes, because I thought I could edit B,
I said, OK, I can just delete the line in B instead.
And somehow, logically, that's a little easier to think about.
But these are exactly dual to one another.
So, in that case, I have x ij.
Well, I still have to deal with A i.
I haven't gotten rid of it.
But I've matched line j.
So I paid $1 for inserting a line.
And now I have that because I've gotten rid
of a line in the other file.
If I stopped here, by the way, I would have it at a distance.
But, unfortunately for me, I have one additional case,
which is mild irritant, as they say, which is that I can swap.
First of all, can I always swap?
I mean, I can, but if I swap two lines
and they still don't match the lines on the right-hand side,
I'm kind of hosed, right, because you can convince
yourself that in the next step, I'm
going to have to delete something anyway.
Swapping was free.
If I swap and delete, that's the same thing as just deleting,
so it doesn't really matter.
So, in particular, what that means
is I might as well only check the swap if it actually
helps me.
Yeah?
So, in other words, if I have A--
now you have to be a little bit careful because I'm swapping.
So if the next guy in A equals B, the current guy in B
and the current guy in A equals the next guy in B. Well,
now I can swap this guy and kill off two lines in my files
while I'm at it, right?
So, in this case, I get that X ij.
Well, swapping doesn't cost me anything,
and I killed off two things.
So that's the recursion.
So if I were to write this out on my homework,
what should I do?
Well, I shouldn't-- I mean, probably if you use this error
notation, I don't think it would be a big deal.
But really, you should add a line
at the bottom saying that I can choose
to do any of these things.
So really, my recursive call is x
ij gets the min of all of these 1,
2, 3, 4 expressions that I've written here.
OK.
AUDIENCE: What if you have the first condition but not
the second one?
JUSTIN SOLOMON: What if I have the first condition but not
the second one?
Ah, so that's a great question.
Yeah, so the question was like, OK, well,
what if I can match the next line but not the current one.
Well, there's two different things you could do.
You could either make another case for that.
That's perfectly fine.
In fact, you could do that.
You could do that I matched the second condition, not
the first one, whatever.
You can just enumerate as many things
you want as long as they're all true
and there's a constant number.
Alternatively, you can convince yourself
that actually is unnecessary here because a different--
so that's like swapping.
But then one of those two lines is still a mismatch,
so you're going to have to delete something
in the next step.
So you might as well just delete first, rather than
swap and then delete.
And so that's why that case isn't necessary.
Yeah?
AUDIENCE: [INAUDIBLE] in the first case.
JUSTIN SOLOMON: Exactly.
Exactly.
So if you swapped and you killed a line, then, in effect,
I think it's a combination of case 1 and case 2
here, if you, kind of, expand your recursion out.
But if you're having trouble convincing yourself
of that, that's fine, just add a case here.
Yeah.
Any other questions?
I'm going to ask quickly because this problem makes me nervous.
AUDIENCE: [INAUDIBLE]
JUSTIN SOLOMON: Sure.
OK.
In the worst case, if we've done something wrong,
you can certainly add another case here.
I'll think about it at home.
OK.
So since I've managed to pontificate too long,
let's keep moving here.
AUDIENCE: Can we swap them if they [INAUDIBLE] match?
JUSTIN SOLOMON: Oh, you know, the problem
is it might have been--
AUDIENCE: --used in the final file.
So if you can make swap one match one of them and not
the other, then that's not OK.
JUSTIN SOLOMON: Yeah, because at the end of the day,
the files have to agree.
Like, you have to match B to A.
AUDIENCE: Swap and delete is cheaper
than doing two deletes and two inserts.
AUDIENCE: No, no, no, but the swap and delete
is illegal because you have to use both ones.
That's a condition in the pocket.
JUSTIN SOLOMON: Oh, I'm sorry.
That's a better answer.
So Jason points out that if I swap, then
I can't delete it because the way the problem is written.
So that effectively removes this case.
Otherwise, I think--
I guess Erik is--
AUDIENCE: Actually, there are two important conditions--
JUSTIN SOLOMON: Oh, sorry.
I've managed to totally botch this,
which is totally unsurprising.
Yeah.
So I think the problem also states that if you swap,
the swap has to be useful.
And that's why this additional case
that Erik is asking about where you swap and then you
match one line but not the other is unnecessary.
You might be able to relax that by just adding a case here,
but since the problem doesn't ask it,
I'm not going to think about.
OK.
Right.
So, under all the assumptions of this problem
that I didn't read but are very important to solving
this problem correctly, I believe
we really have written down all of our cases here.
OK.
So let's continue with our SRTBOT paradigm.
So now we have all of our recursion.
The topological order here is a little bit trickier than normal
because now you've got a two-dimensional array,
but it follows a pattern that's pretty typical here,
which is that x ij only depends on other x ij's
with higher i plus j.
So I think about my graph of subproblems.
If I wrote this in a 2D matrix, it always, kind of,
points down and to the right, maybe,
which is what's making it acyclic.
This is a very typical pattern in these sort
of two-dimensional dynamic programming problem.
All right.
So let's see here, SRTBOT.
So we need our base case.
This isn't too bad because, essentially,
when you have boring documents, they're
very easy to match to one another.
So, in particular, for any i, if I'm at line n plus 1--
in other words, I have a blank document that I'm matching
to document i--
well, how much work do I have to do?
You have to be a little bit careful.
This is where the suffix version of this problem is a little bit
more annoying than the prefix one--
or have I managed to swap those backward again--
that, in particular, the remaining number of lines
looks like n plus 1 minus i, which is
different than in the problem.
It's just i because they're working
in the other direction-- in the solution, rather.
And similarly, you need a second case for those two here, right?
So you have x n plus 1 j is going to be n plus 1 minus j.
Cool.
OK.
So we're going to continue with SRTBOT here.
So what is our original case?
Kind of by definition it's x 1, 1 or 0 0,
depending on how you index.
And then, finally, what's our runtime?
Well, let's see, there's n plus 1 squared subproblems,
and, of course, that's equal to order n squared.
The subproblems are just a constant amount of work,
so they're each with constant work.
So our entire runtime is order n squared.
And hopefully by watching me be confused in front of you
and think through this problem, you too
will see how the problem solving procedure
can happen in your own disorganized brains.
OK.
So that concludes our treatment of this problem here.
That, I think, is the hardest one.
So the other two, thankfully, are much easier to think about,
I thought.
But I never liked edit distance.
I remember seeing that and undergrad algorithms getting
confused.
OK.
So the next problem, problem 3 here, deals with Saggy Mimsin.
And she has a bunch of block, and she
wants to stack them on top of each other, as one does.
And as a young structural engineer,
she has a few criteria on her problem.
Let me go to the right page in my notes here.
Right.
So this is problem 3.
So we have that block bi has size that looks like width wi
by height hi by length li.
I remember getting confused in elementary school
about the difference between width and length all the time.
To me, those always sounded the same.
But it doesn't really matter, because she's
happy to rotate her cubes any way that she pleases.
There's a key detail which I did remember to actually read
in this problem, which is that she has at least three
of each type, where type here means that I can permute
these three numbers any way that I
want because that's the same as just rotating a block.
But any time she has one block that's like 1 by 2 by 3,
she has at least two more in her bag somewhere.
OK.
APPLE WATCH: It's 6.
JUSTIN SOLOMON: Oh, 1 times 2 times 3 is equal to 6.
Thank you, Apple Watch.
OK.
So that's odd.
So she can orient her block any way
that she wants, meaning she can rotate it in any fashion
that she'd like.
And so what we're trying to do, what we want
is the max height where she's stacking her n blocks.
I suppose I should say they're n blocks.
So she wants the max height that she can achieve.
But just to be kind of annoying, or because, again,
she's very concerned with structural stability--
she lives in an earthquake zone--
she would like with the condition
that each block is strictly supported on the block
beneath it.
Right?
So in other words, if this is the base of one block,
then the next block that's stacked on top of it
has to be strictly contained within the block below it.
Right?
So does the problem make sense?
Have I omitted any critical details?
I don't think I have this time.
This one's a little easier.
AUDIENCE: [INAUDIBLE]
JUSTIN SOLOMON: Oh, yeah.
And she can't do anything crazy.
She can't do a weird, like, balance it
on this edge kind of thing, which--
Erik is absolutely right-- could actually give her a taller
tower than you could get if you're only allowed
to rotate blocks 90 degrees.
I don't think the problem states that explicitly, but this
isn't a trigonometry class, so I think we're in good shape.
OK.
Right.
So that's our basic problem here.
And this is one of these problems that
is going to be a dynamic programming problem,
but, again, similar to many of the things that we
saw in lecture, is not totally obvious how,
because somehow she has this big, disorganized bag
of blocks.
You could imagine a universe where
there's 2 to the n different things she could do, right?
For every single block, she could
decide whether or not to put it in her stack,
and then she has to do a bunch of other work
to verify whether she can stack them
while supporting the strict support condition or not.
So, initially, that seems kind of annoying.
So what we have to do, which, again,
is pretty common to a lot of these problems,
is place some order on it.
I mean that both in the entropy sense,
and also, like, literally, were going to order stuff.
And, in particular, we'll see that this problem has
a lot in common with that longest
common subsequence problem that we saw in lecture-- increasing
subsequence, sorry.
Right.
So here's some observations about our problem
which are going to help.
First of all, when we stack our blocks,
we might as well always align the shorter side of the block
on top to the shorter side of the block underneath it.
Right?
Let me draw a picture of what I mean.
So let's say I have a really--
a block whose base kind of looks like that
and then another block which is also rectangular
that I sit on top of it like that.
Then notice I could-- so in this case, the shorter edge of one
block is aligned to the longer edge of the other.
Notice I can rotate it 90 degrees and it
still supports on one another.
So there's never a case--
you convince yourself with just a few inequalities--
where I don't, kind of, always put the long side parallel
to the long side of the guy underneath and the short side
parallel to the short guy underneath it.
Does that makes sense?
Cool.
So that's observation one.
Observation two, can they ever-- like,
let's say that Maggie actually-- sorry,
Saggy actually had not just three blocks of a type,
but like 25.
So she's just has hella blocks.
My question is does it matter.
The answer is no because this word here is really critical,
which is that there's strict support.
So your block only has so many faces.
And, in fact, by observation one,
really all that matters is which of the three types of faces
is sitting on top because we can just always rotate it.
So there's three configurations of every block, so, at most,
can any one configuration appear more than one time?
No, because of the strict support condition.
Right?
Otherwise, the rectangles would match up,
and that's against the rules.
So, in particular-- oops, the number after two is--
the number after 1 is 2, which looks like that.
OK.
Right.
So, in particular, there are only three orientations.
This is just which of the three edges of the block
is the one that's going away from the floor, the normal
to the ground.
And moreover, each can appear less than or equal to 1 time.
That's good because it limits the size of our problem.
And, finally-- oops, I collapsed two of the cases in my notes
into one case here.
But that's OK.
And, in fact, notice that the problem tells us
that she has at least three of each type.
So, in a sense, if the problem--
if you observe one of a block you might as well just
throw away the rest because you know that you
can use it at most three times.
And she has three of that block.
We can't use it more than three times, so in a sense,
that's just superfluous information.
OK.
Right.
So this allows us to put a little bit of order
here, because notice that when I look at the stack of blocks
here, what do we know?
If I look at the length of the long side
and the length of the short side in the plane of the ground,
those numbers have to decrease on every level of my block.
They can never increase.
That's what the strict support conditions says,
combined with observation one, actually, even
without observation one, which is good news, right?
So this is what's going to allow us to impose order
on our problem, namely, that we can sort by the edge lengths
because we know that we have this support condition.
OK.
So let's fill in some details of our algorithm.
OK.
So originally-- already we can see that our list of blocks
is kind of useless because the width, length,
and values are sorted in ways that don't matter.
Moreover, if we have more than three of a given block,
that's somehow not super useful.
So, instead of that, without loss of generality,
let's assume--
so WLOG here-- we can always take our block and assume--
I'm going to do this slightly different from my notes--
that the width is less than or equal to the height is less
than or equal to the length.
OK.
So every block, if this isn't the case,
I could go down my array of blocks and sort.
And sorting a list of three numbers is constant time.
OK?
Right.
So what does this allow me to do?
Well, I'm going to say that a block type actually
is an ordered set where the third number is going
to be the axis that points up.
And the reason to do that is that we
know that we can never use that more than once
for any type of a block.
Yeah?
So now I'm going to make a new list of blocks with a capital B
because I like blocks.
And it's going to look like the following.
So if width-- so if w is less than h is less than l,
then I'm going to take every block
and duplicate it three times.
Notice that I might end up with a list with, like, nine
times of every block, but we'll fix that later.
Right.
And it's going to look like the following, which
is that, OK, I'm going to have wi, hi, li.
This is like describing a way to stack my block because it's
saying this is the short side, this is the long side,
this is the vertical side.
And there are three cases where any one of these guys
can be the vertical side.
So there's one.
Let's say that the h is the vertical side,
then w has to go before l.
So it would be wi, li, hi, and a third one
where the third guy is w.
H is less than l, so it would be hi, li, wi.
And those are all the different ways
that I can, sort of, orient these blocks in my stacking,
assuming that I impose condition one for convenience here.
OK.
I'm going to make a new list of blocks
where I take every block in my original set
and I just duplicate it three times this way
after I sort its coordinates.
And now, well, what do I need to do?
For one thing, this thing may have too many blocks.
I might have a block that's repeated more than one time
and I can't do that.
And moreover, it's going to be convenient to have
this sorted because I've got to stack these guys eventually.
Yeah?
So I'm going to sort that list.
And I want to do it--
I can never say this word-- lexicographically,
meaning that I'm going sort out the first coordinate, and then
the second and the third lexicographically.
Notice this length is 3 n if I had n blocks to start with.
So this entire thing takes order n log n time, which
is important to account for.
And then I can remove duplicates.
I'll let you guys convince yourself
you can do this in order n time.
An easier way would've been making second array and just
kind of move--
and only add stuff when you didn't
see the same thing before.
OK.
And, finally, now these are ordered in a really nice way
because I can stack my blocks, but only
ever looking to the right in my sorted
list, assuming that I'm stacking from the top of my tower down,
which is, I think, sort of what's going on in this thing.
OK.
So now, finally, we can do our SRTBOT.
And I might do S and R and T and then allow you
guys to think about the rest because, as usual,
I'm talking too much.
OK.
So now this is starting to look like a subsequence problem
because, essentially, when I stuck my blocks, if I
use this block here--
again, if I'm stacking from my tower from the top down--
all the blocks that can sit underneath this one
have to be further to the right in my array because of the way
that I sorted.
Now, that doesn't mean that I can put anything
on the right underneath this guy,
but it does mean that I know nothing to the left
can go underneath this guy.
That's the way to think about it.
OK.
So it is going to be SRTBOT.
So S, what I'm going to say is that x i here
is equal to the maximum height of my tower, and I'm going to--
taking a little bit of inspiration
from our subsequence problem that we've already seen,
I'm going to force myself to use block i.
We'll see if that's convenient.
i and possibly-- just for fun, maybe we'll
do the prefix version of this problem this time.
So now I can use any of the previous blocks.
So I can use the first i blocks to make a tower,
but I'm forced to use block i.
By the way, from now on, when I use indices,
it's into this sorted array.
OK.
So this is a problem.
Obviously, if I could solve for x,
I would be done, because I could get the maximum height
by just iterating over all the x's and choosing the biggest
possible value here.
And the question is, how do I do this recursively?
OK.
So here's our recursive step.
So let's say that I use block i, well,
because we know we have to.
Right?
So, in particular, we have--
now I'm seeing why they didn't use this notation
in their answer, but that's OK.
Let's use another letter to refer to the third coordinate.
AUDIENCE: v i for vertical [INAUDIBLE]..
JUSTIN SOLOMON: Yeah, let's say v i is always
the third coordinate.
We've already used w, h, and l, and I'm
afraid if I reuse them after sorting,
it's going to confuse people.
So v i is the third coordinate of the i-th element
of my sorted array.
That's fine, OK.
Right, so what is my height if I use x i here?
Well, I get some height from vi.
And in addition to that, I get whatever
I stack underneath that guy.
So, in particular, I get that xi.
Well, I get the height of the block
that I just decided to use.
And now, what are all my cases?
Well, I could decide to do nothing else,
like, just not use any other blocks.
That gives me a height of 0.
Or, well, let's see here.
I could use the x's, but I have to be careful
that I can actually stack them.
So, in particular, well, I need--
yeah, I can take an x j value, but I've
got to be careful that I can stick it underneath.
So, in particular, what do we know?
Well, I can do anything from 1 to i minus 1
because that's sort of the definition of xi.
But, in particular, I can stack it on top.
So one easy way to do this is I just array--
I iterate through the first i minus 1 elements of my array,
and I just check my stacking condition
for every single one of them relative
to block j, so, in other words, that the width and the height--
or rather, the first and the second coordinate
satisfy the strict inequalities that I need.
I'm phrasing this sentence neutrally
because I forget whether this is increasing or decreasing.
But in any event--
so what do I do?
I check all of the blocks that I could possibly
stack from the index of the array perspective.
I make sure that I could actually stack them,
thanks to the size of the current block
that I just decided to add to my stack.
And I move recursively.
OK.
Right.
So this is great because now we're
in exactly the recursive scenario we wanted
to be in, because x i only depends on x j, where
j is smaller than i.
And that is exactly our topological
ordering that we need.
If you do that on your homework, you get a minus n for large n.
OK.
Similarly, what's our base case?
Well, obviously, if I only have one block,
I might as well use it.
So in that case, we have x1 is equal to v(1) our notation here
like that.
Our original one, we have to be a little bit careful because
of the way that I've defined x, because x assumes that I've
used a particular block, so I have to say,
well, I might not have actually chosen the very last block
as the one I want to keep.
So I have to iterate.
I can say that, really, my original
is the max over i of x i.
So one of these blocks has to be the block on top.
I'm just going to iterate over all the possible ones
and find it.
And then our final thing to do is the runtime t.
This one is mildly trickier than the previous runtimes
that we've done so far in our example problem.
In particular, how many subproblems are there?
Well, there's n subproblems--
or I'll say order n because I'm always off by 1--
corresponding to each block in my stack here.
But how much time does each subproblem
take, at least the way that I've written it here?
Well, what I have to do?
I have to loop over all of the possible blocks
and find the one that I can stack on top of
and then take the max.
So there's a loop here from 1 to i. i is upper bounded by n.
So this is order n subproblems times order n
work per subproblem.
So at the end of the day, my algorithm
is going to be order n squared time.
And, of course, again--
I guess I promised it and then I didn't actually do it--
to actually implement this algorithm,
there's sort of two different ways to do it.
I could write a recursive call plus a table.
The tables maybe initialized to a bunch of NaNs.
And then I implement this function recursively.
But before I do that, I say, if the table does not equal NaN,
just return the value in the table
and otherwise call this recursion.
Or I can just have a four loop from 1 to n
and build up the table one element at a time.
And both of those are exactly the same
from a runtime perspective.
OK.
So I think I've managed to watch that much
more than my notes or the written solution,
but the problem itself is actually
pretty straightforward.
So if you guys read through the answer plus some of--
I think, actually, the hard parts of this problem
were not the dynamic programming,
it was all the observations you need to get there.
So that's why I spent a little more time there.
OK.
So, as usual, I haven't left myself enough time
for the last problem.
But we have a few minutes and that'll
be sufficient to set up the parts.
I actually found the last problem
to be easier even though it technically is, sort of,
two dynamic programs in one.
So I think the logic is a little easier.
OK.
So
AUDIENCE: Use the backboard [INAUDIBLE]..
JUSTIN SOLOMON: I think this is the backboard.
Yeah, I was just realizing that this room doesn't work
the same way as the other one.
Yeah, this is embarrassing.
You know, I spent all day thinking about topology,
and this is like a classic kind of problem in that universe.
OK.
Well, we'll just erase one board at a time,
and I'll try not to write three feet wide this time.
Oh, this is probably the one board I shouldn't use.
I don't think I like this classroom.
OK.
So, right, in our final problem, we're given an n-by-n grid.
And on our n-by-n grid, Princess Apple--
Banana--
AUDIENCE: Plum.
JUSTIN SOLOMON: --Plum.
Princess Plum.
Right, so here's our basic setup.
There's a big grid of stuff, or maybe a small grid
because I don't feel like drawing.
And every grid square can have one of three things.
We can have a mushroom.
We can have a tree.
Or it can have nothing at all.
And our princess starts here, and she goes--
she wants to go there.
And moreover, there's a couple of things
that are worth noting here.
So, first of all, her path is quick, meaning
that she can only traverse 2n minus 1 grid squares
to get from one corner to the other.
And, apparently, she's very into mushrooms,
and she'd like to accumulate as many as possible
along her path.
That's the basic setup here.
So she wants to get from the upper left to lower right.
And, in order to do so, she wants to take a quick path.
Her main priority is to be efficient.
But among the different quick paths,
she wants to pick up a lot of mushrooms.
It's understandable.
AUDIENCE: And not walk through trees.
JUSTIN SOLOMON: And not only through trees, thank you.
So maybe there's some grid squares
that are marked with a tree, meaning that you just
can't go there.
That's a tree.
OK.
Right, so that's our basic setup here.
But the problem does--
it takes a bit of a twist, right?
It's not saying just could be the shortest path, which
would be very much like the last kind of unit in 6.006.
But rather, the question is, sort of,
what is the number of paths that she can take from one side
to the other and what is the maximum number of mushrooms, is
roughly the question asking, or at least
what I remember from reading it last night.
AUDIENCE: The number of paths that maximizes the same number.
JUSTIN SOLOMON: That's right.
So she has to take the most number of mushrooms
she can, but there may be more than one path that
gets you there that is quick, that
satisfies this condition, in which case,
she wants the count of the total number of ways
you could get from one corner to the other.
Why, you might ask--
why not?
OK.
So, right.
So, for instance, maybe there's a mushroom here.
Now there's a quick path that gets her there
and collects one mushroom.
So there's exactly one.
But maybe if there's a mushroom there, well, initially,
it feels like maybe she could get two mushrooms.
She could go there, go pick up the second mushroom,
and get back.
But we're going to see that this quick condition actually
allows-- it doesn't allow you to do that.
OK?
So, in fact, it'll turn out that quick paths can only
collect one mushroom in this 3-by-3 case,
so there's at least two different paths.
Well, there's 1, 2, 3 different ways
that she could collect one mushroom and have a quick path.
OK.
So the first thing to notice is the instructions
are a little bit sneaky by defining quick paths,
basically, by giving her no slack at all.
Right?
And here's a basic observation.
Notice that in order to give from the upper left
to the bottom right, she's going to have
to go down and to the right.
Plausibly, she could also go up.
She could try and go around a tree, but only plausibly.
And, in particular, the question is how many times does she
have to go down and how many times did she
have to go to the right.
Well, she has to get to the bottom of the grid.
So she's on grid square number 1.
She has to go down, in this case, at least two more
times, so, in general, n minus 1 times.
She has to go to the right n minus 1 times.
So what does that mean?
She has to make 2n minus 2 moves.
And that's a lower bound, right?
So if she goes up, she's going to have to go down again,
so it's only going to make it bigger.
Right?
So at the very least, she has to do 2n minus 2 moves
down and right to get from the upper left to the lower right.
How many squares does she touch when she's doing that?
This is a fence post problem.
So she made 2n minus 2 moves, and she
had a place where she started.
That implies that just by moving down and to the right,
she makes 2n minus 1 squares--
she touches, rather.
So can she ever move up?
No.
Can she ever move to the left?
No.
And that, basically, is all you need to solve this problem.
The rest of it's actually pretty easy.
So the basic observation here is she can only move down
and to the right because if she moved up or to the left,
her path would no longer be called quick,
and that would be a problem.
Moreover, every path that moves down and to the right
is a quick path, assuming she reaches their target
and doesn't hit a tree.
OK?
So that's the basic observation.
And notice that that already basically suggests--
it's, like, screaming out to you--
how to do dynamic programming because, literally, you have
a table looking at you on the blackboard right now,
and you have an ordering down and to the right that
is acyclic.
Yeah?
OK.
Have I slammed on the board enough times?
The first time I taught at Stanford,
I got negative course feedback that I had too much coffee
and was slamming on the board a lot, apparently.
I watched the video later, and, indeed, that was not wrong.
OK.
So, right.
So, we're going to call k--
this is going to be the max mushrooms she
can get going on the entire path from the upper left
of the lower right.
So we want to know the number of quick paths
that can achieve this number k.
OK.
So let's do SRTBOT really fast because I've got four minutes--
actually, a tiny bit more than that because we started late.
OK.
Now, the kind of annoyance here is
that there's two different numbers that we don't know.
One of them is k, and the other is the number of paths.
The problem didn't tell you how many mushrooms she can pick up.
It does tell you that there's some path
to get from the upper left to the bottom right,
that there's not just like a row of trees
somewhere, which I feel in my commute sometimes.
But it doesn't tell you the number
that she has to accomplish.
And, initially, that's kind of annoying.
So maybe the first thing that we do
is just compute k, like, the maximum number
of mushrooms that she can collect on any quick path.
And then we go back and compute that other count.
That would be one problem-solving approach
that we could think about a little bit.
So, in particular, let's define our k ij to be equal to--
well, we can generalize our problem slightly,
and say what is the number of mushrooms
that I can get on any kind of rectangle embedded inside
of my full problem, right?
So, in other words, this is the max number of mushrooms,
or m's for short, on a quick path to ij.
So, in other words, she always starts in the upper left,
but now she stops at any other grid cell.
OK?
Because I'm running low on time--
no, I'm going to do this the way I want to do this.
No, so we're just going to think about k.
Yeah?
So the question is could we compute just
as value k, which certainly seems convenient.
Princess Plum, she might as well know her enemy.
She might as well know the number of mushrooms
she's targeting, if she can get it.
So how could we do this recursively?
Well, she has to get to position ij.
And from our argument up there, she
has to get there by either coming from up or to the left,
the way we've chosen to write down this problem.
So what are our different cases?
Well, first of all, if there's a tree,
you can't do a damn thing.
She shouldn't even be able to get there.
And for convenience, we're going to find
you can argue that it's 0.
We're going to mark this with a special number,
and we'll see that that makes our notation
a little convenient.
So one is if there exists a tree,
then we're going to say k ij is minus infinity.
Again, there's a philosophical question there.
Does she get minus infinity mushrooms
if she is standing on top of a tree?
I don't know, because she shouldn't
stand on top of a tree.
But at least it's going to let us know that something
went wrong in this grid square in the other parts
of our recursion.
OK?
And otherwise, well, what are our different cases here?
Well, she always picks up a mushroom if it's there.
She might as well.
She's maximizing.
In fact, the problem even says that she's really
into mushrooms.
She collects them automatically, right?
So what do we get?
I use unnecessarily fancy notation.
This is an indicator of whether there exists a mushroom
at position ij, meaning this is a 1 if there is,
and there's a 0 if there's not.
Sometimes this is indicated with a 1 with a little subscript,
but whatever.
In addition to that, she might have picked up
mushrooms along paths.
And we know that her path to position ij either
came from up or to the left.
So, in other words, we know that she
could have gotten the max from any path ending above her
or any path to the left of her.
So that's k i minus 1j, which, I guess,
is to the left, and k ij minus 1, like that.
And this can be used to fill in our whole table of k values.
In fact, since I'm low on time, I'll
let you do the TBOT for the remainder of this problem.
Essentially, I think the key observation is this one.
Obviously, when she starts in the upper left,
she gets 0 mushrooms because she's not
standing on top of one.
The problem says that.
And this allows us to fill in our whole table k.
So, in particular, this gives us our enemy now.
We now know how many mushrooms she should have
at every step of her journey.
In fact, it tells us a tiny bit more
than that because it says if I'm at this grid
sq-- at this particular grid square during my path,
I should have this many mushrooms.
If I didn't, then something went wrong.
Yeah?
So the way that the solution is written,
they do two pieces of the recursion at once.
You actually could have just solved for this k matrix
first and then gone back and done the second half
of this problem.
And those are exactly the same.
And when I was writing my solution,
this is the way I thought about it, because, somehow, I
kind of felt like she might as well know
how many mushrooms she wants to collect before she starts
counting paths.
That's like a secondary question, you know.
And so this is one way to do it.
So in our remaining negative 2 minutes,
let's think about the recursion for computing.
Remember that we want to know the number of paths needed
to collect that many mushrooms, the maximum number
of mushrooms.
Please let there not be a whole lot of stuff on this board.
Ah, there's not any stuff on this board.
That's great.
OK.
So, in particular, now I'm going to define a second thing
that I also I'm going to do dynamic programming on.
I'm going to say that x ij is equal to-- and I'm going
to make it kind of a sneaky definition here,
which is the number of quick paths that end at ij with--
now, let's anticipate our problem a little bit.
So at the end of the day, we're going to do x of n comma n
because she wants to end up all the way down and to the right.
And how many mushrooms does she want to have,
now that we know k?
She wants to have k of n comma n mushrooms.
Along the way, it would be kind of ambitious
if she wanted to have k of n comma n
mushrooms the entire path.
But it would be slightly less ambitious to have
k of ij mushrooms because, somehow, that's
exactly what we just constructed in the previous thing
where it was paths.
Well, the last guy looks like a max.
Now we're going to expect to see some plus signs here
because we're adding up how many paths we have.
OK?
And now let's come up with a recursive rule for the array x,
and then we'll call it a day.
So, in particular, [INAUDIBLE] one, if there's a tree,
how many paths are there?
There are no paths, because I can't get there.
So then ij equals 0.
OK?
Otherwise, there ain't a tree.
And now I have to be a little careful, right?
So I need to write this like a little piece of code.
You could have written this as a giant max, instead, or a bunch
of cases and whatever.
So let's think about it like a piece of code.
So, initially, I think there are no paths that
get me k ij mushrooms.
That's perfectly fine.
And remember, we're going to keep
applying the same piece of logic,
which is that a path can only come from to the left and up.
And let's think about those two cases.
And by the way, we're going to use chi
to equal this chi of stuff that we
had in the previous expression.
So chi is 1 if there's a mushroom at this place,
and 0 if there is not.
OK.
So my path can come from the left or up.
I know that it can't come from up if the number of mushrooms
that I got from up plus, potentially, the one that I got
here doesn't align with the number of mushrooms
that I should have by the k ij standard
that I have set for myself.
So if I want to write that out in code, the way I do that is I
would say, if k of--
so let's say I look to the left first.
This is like, you know, look to your left, look to your right,
one of you will pass this exam kind of scenario.
And I potentially add a mushroom at my current position
if there is one.
If that is equal to k ij, well, what does that mean?
That means that paths that went to the left
were able to collect the number of mushrooms I need to get
to the position I am now.
So now I can add all of the different ways.
Maybe I'll do a plus equals x of i minus 1j
because any path that got to the previous guy
and collected the right number of mushrooms
can now reach me and get the right number of mushrooms.
And similarly, I can look up and do exactly the same logic.
So if k of ij minus 1 plus this number is equal to k ij,
then x ij gets an additional number of paths, like that.
And now I do think it's worth spending 8 seconds thinking
about our base cases here because, initially,
when I first saw this, I panicked a little bit
because it kind of looks like this would just end up
getting a bunch of 0's because I'm just adding
values of x's to themselves.
I don't have, like, a 1 plus anything
anywhere, which is kind of weird if you think about it.
So all of the reason why the positive numbers appear
in this problem is from the base case, which is kind of cool,
I think.
This is like, I think, one of these things where
if you anticipated a problem, then it's cool,
and if you didn't anticipate the problem to begin with
and you just wrote down these formulas,
you probably wouldn't even think that it's interesting.
But in any event, what is our base case?
So, we'll do the B in SRTBOT.
So, first of all, what is k of 1, 1?
Remember, that's the number of mushrooms
she can collect by starting at the left square
and going nowhere.
And that's 0 because the problem says there's
no mushrooms in the upper left.
What's x of 1, 1?
Well, this is the number of paths from 1,
1 to itself that collects 0 mushrooms, so that's 1.
OK.
And I think that the rest of the SRTBOT table
here isn't terribly difficult to fill in.
So I noticed that in kind of a funny way,
all of these recursive steps are just adding 1
to itself a bunch of times.
But, of course, the way you do that, the reason why
you get a number that is interesting,
is because of all these if statements and the fact
that you can add two different pluses coming
from two different sources.
OK.
So I actually do encourage you guys
to look at the code in the problem solution
because I think it's a nice example of taking
this recursive formula and then unrolling it into,
like, iterating over a table.
And that's a useful skill that I intended to do today
and then didn't actually do very carefully.
But with that, as usual, we've gone over time here.
So we'll call it for the day.
And I will see you guys when I see you guys.
All right.