You are on page 1of 17

Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

Dynamic Programming – 7 Steps to


Solve any DP Interview Problem
May 15, 2018 15 min read

Nikola Otasevic

Nikola (co-founder of Refdash) is a former Google Engineer. He has conducted


1000s of technical interviews at Google and at startups he has been involved
with. Previously, Nikola got his masters and bachelors from MIT.

Despite having significant experience building software products, many


engineers feel jittery at the thought of going through a coding interview that
focuses on algorithms. I’ve interviewed hundreds of engineers and some of
the most common questions that make engineers uneasy are the ones that
involve Dynamic Programming (DP).

Many tech companies like to ask DP questions in their interviews. While we


can debate whether they’re effective in evaluating someone’s ability to
perform in an engineering role, DP continues to be an area that engineers trip
on their way to finding a job that they love.

Dynamic Programming – Predictable and Preparable

One of the reasons why I personally believe that DP questions might not be
the best way to test engineering ability is that they’re predictable and easy to
pattern match. They allow us to filter much more for preparedness as
opposed to an engineering quality.

These questions typically seem pretty complex on the outside and might give
you an impression that a person who solves them is very good at algorithms.
Similarly, people who may not be able to get over some mind-twisting
concepts of DP might seem pretty weak in their knowledge of algorithms.

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 1 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

The reality is different and the biggest factor in the performance is


preparedness. So let’s make sure everyone is prepared for it. Once and for all.

The biggest factor in solving dynamic programming problems is


preparedness.

7 Steps to solve a Dynamic Programming problem

In the rest of this post, I will go over a recipe that you can follow to figure out
if a problem is a “DP problem”, as well as to figure out a solution to such a
problem. Specifically, I will go through the following steps:

1. How to recognize a DP problem


2. Identify problem variables
3. Clearly express the recurrence relation
4. Identify the base cases
5. Decide if you want to implement it iteratively or recursively
6. Add memoization
7. Determine time complexity

Sample DP Problem

For the purpose of having an example for abstractions that I am going to


make, let me introduce a sample problem. In each of the sections, I will refer
to the problem, but you could also read the sections independently of the
problem.

Problem statement:

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 2 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

In this problem, we’re on a crazy jumping ball, trying to stop, while avoiding
spikes along the way.

Here are the rules:

1) You’re given a flat runway with a bunch of spikes in it. The runway is
represented by a boolean array which indicates if a particular (discrete) spot is
clear of spikes. It is True for clear and False for not clear.

Example array representation

2) You’re given a starting speed S. S is a non-negative integer at any given


point and it indicates how much you will move forward with the next jump.

3) Every time you land on a spot, you can adjust your speed by up to 1 unit

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 3 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

before the next jump.

4) You want to safely stop anywhere along the runway (does not need to be at
the end of the array). You stop when your speed becomes 0. However, if you
land on a spike at any point, your crazy bouncing ball bursts and it’s a game
over.

The output of your function should be a boolean indicating whether we can


safely stop anywhere along the runway.

Step 1: How to recognize a Dynamic Programming


problem
First, let’s make it clear that DP is essentially just an optimization technique.
DP is a method for solving problems by breaking it down into a collection of
simpler subproblems, solving each of those subproblems just once, and
storing their solutions. The next time the same subproblem occurs, instead of
recomputing its solution, one simply looks up the previously computed
solution. This saves computation time at the expense of a (hopefully) modest
expenditure in storage space.

Recognizing that a problem can be solved using DP is the first and often the
most difficult step in solving it. What you want to ask yourself is whether your
problem solution can be expressed as a function of solutions to similar
smaller problems.

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 4 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

In the case of our example problem, given a point on the runway, a speed, and
the runway ahead, we could determine the spots where we could potentially
jump next. Furthermore, it seems that whether we can stop from the current
point with the current speed depends only on whether we could stop from the
point we choose to go to next. That is a great thing because by moving
forward we shorten the runway ahead and make our problem smaller. We
should be able to repeat this process all the way until we get to a point where
it is obvious whether we can stop.

Recognizing a Dynamic Programming problem is often the most difficult step


in solving it. Can the problem solution be expressed as a function of solutions
to similar smaller problems?

Step 2: Identify problem variables


Now we have established that there is some recursive structure between our
subproblems. Next, we need to express the problem in terms of the function
parameters and see which of those parameters are changing. Typically in
interviews, you will have one or two changing parameters, but technically this
could be any number. A classic example of a one-changing-parameter
problem is “determine an n-th Fibonacci number”. Such example for a two-
changing-parameters problem is “Compute edit distance between strings”. If
you’re not familiar with these problems, don’t worry about it.

A way to determine the number of changing parameters is to list examples of


several subproblems and compare the parameters. Counting the number of
changing parameters is valuable to determine the number of subproblems we
have to solve, but it is also important in its own right in helping us strengthen
the understanding of the recurrence relation from step 1.

In our example, the two parameters that could change for every subproblem
are:

1. Array position (P)

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 5 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

2. Speed (S)

One could say that the runway ahead is changing as well, but that would be
redundant considering that the entire non-changing runway and the position
(P) carry that information already.

Now, with these 2 changing parameters and other static parameters, we have
the complete description of our sub-problems.

Identify the changing parameters and determine the number of


subproblems.

Step 3: Clearly express the recurrence relation


This is an important step that many rush through in order to get into coding.
Expressing the recurrence relation as clearly as possible will strengthen your
problem understanding and make everything else significantly easier.

Once you figure out that the recurrence relation exists and you specify the
problems in terms of parameters, this should come as a natural step. How do
problems relate to each other? In other words, let’s assume that you have
computed the subproblems. How would you compute the main problem?

Here is how we think about it in our sample problem:

Because you can adjust your speed by up to 1 before jumping to the next
position, there are only 3 possible speeds and therefore 3 spots in which we
could be next.

More formally, if our speed is S, position P, we could go from (S, P) to:

1.
1. (S, P + S); # if we do not change the speed
2. (S – 1, P + S – 1); # if we change the speed by -1
3. (S + 1, P + S + 1); # if we change the speed by +1

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 6 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

If we can find a way to stop in any of the subproblems above, then we can also
stop from (S, P). This is because we can transition from (S, P) to any of the
above three options.

This is typically a fine level of understanding of the problem (plain English


explanation), but you sometimes might want to express the relation
mathematically as well. Let’s call a function that we’re trying to compute
canStop. Then:

canStop(S, P) = canStop(S, P + S) || canStop(S – 1, P + S – 1) || canStop(S +


1, P + S + 1)

Woohoo, it seems like we have our recurrence relation!

Recurrence relation: Assuming you have computed the subproblems, how


would you compute the main problem?

Step 4: Identify the base cases


A base case is a subproblem that doesn’t depend on any other subproblem. In
order to find such subproblems, you typically want to try a few examples, see
how your problem simplifies into smaller subproblems, and at what point it
cannot be simplified further.

The reason a problem cannot be simplified further is that one of the


parameters would become a value that is not possible given constraints of a
problem.

In our example problem, we have two changing parameters, S and P. Let’s


think about what possible values of S and P might not be legal:

1. P should be within the bounds of the given runway


2. P cannot be such that runway[P] is false because that would mean
that we’re standing on a spike
3. S cannot be negative and a S==0 indicates that we’re done

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 7 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

Sometimes it can be a little challenging to convert assertions that we make


about parameters into programmable base cases. This is because, in addition
to listing the assertions if you want to make your code look concise and not
check for unnecessary conditions, you need to also think about which of these
conditions are even possible.

In our example:

1. P < 0 || P >= length of runway seems like the right thing to do. An
alternative could be to consider making P == end of runway a base case.
However, it is possible that a problem splits into a subproblem which
goes beyond the end of the runway, so we really need to check for
inequality.
2. This seems pretty obvious. We can simply check if runway[P] is false.
3. Similar to #1, we could simply check for S < 0 and S == 0. However, here
we can reason that it is impossible for S to be < 0 because S decreases by
at most 1, so it would have to go through S == 0 case beforehand.
Therefore S == 0 is a sufficient base case for the S parameter.

Step 5: Decide if you want to implement it


iteratively or recursively
The way we talked about the steps so far might lead you to think that we
should implement the problem recursively. However, everything that we’ve
talked about so far is completely agnostic to whether you decide to
implement the problem recursively or iteratively. In both approaches, you
would have to determine the recurrence relation and the base cases.

To decide whether to go iteratively or recursively, you want to carefully


think about the trade-offs.

Recursive Iterative
Asymptotic time Same assuming
Same
complexity memoization

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 8 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

Memory usage Recursive stack, Full memoization


Sparse memoization
Often faster
Slower, needs to do same work
Execution speed depending on the
regardless of the input
input
No issues as long as enough
Stack overflow Problem
memory for full memoization
More intuitive / Often easier to most people find it harder to
easier to implement reason about reason through

Stack overflow issues are typically a deal breaker and a reason why you
would not want to have recursion in a (backend) production system. However,
for the purposes of the interview, as long as you mention the trade-offs, you
should typically be fine with either of the implementations. You should feel
comfortable implementing both.

In our particular problem, I implemented both versions. Here is python


code for that:
A recursive solution:

def
canStopRecursive(runway,
initSpeed, startIndex = 0):
# negative base cases
need to go first
1if (startIndex
def canStopRecursive(runway,
>= initSpeed, startIndex = 0):
len(runway) or startIndex
< 0 or
2 initSpeed
# negative
< 0 or notbase cases need to go first
runway[startIndex]):
3#return ifFalse
(startIndex >= len(runway) or startIndex < 0 or
base case for a
stopping condition
4ifreturn
initSpeed == 0:
initSpeed < 0 or not runway[startIndex]):
True
# Try all possible paths
5for adjustedSpeed
return Falsein
[initSpeed, initSpeed - 1,
initSpeed + 1]:
6# Recurrence
# baserelation:
case for
If a stopping condition
you can stop from any of
the subproblems,
7# you ifcaninitSpeed
also stop == 0:
from the main problem
if canStopRecursive(
8 runway, return True
adjustedSpeed, startIndex
+ adjustedSpeed):
9 return#TrueTry all possible paths
return False

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 9 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

10 for adjustedSpeed in [initSpeed, initSpeed - 1, initSpeed + 1]:

11 # Recurrence relation: If you can stop from any of the subproblems,

12 # you can also stop from the main problem

13 if canStopRecursive(

14 runway, adjustedSpeed, startIndex + adjustedSpeed):

15 return True

16 return False

An iterative solution:

def
canStopIterative(runway,
initSpeed, startIndex = 0):
# maximum speed
cannot be larger than
length of def
the canStopIterative(runway,
runway. We initSpeed, startIndex = 0):
will talk about
1# making#this bound
maximum
tighter later on.
speed cannot be larger than length of the runway. We will talk
maxSpeedabout= len(runway)
if (startIndex >=
2
len(runway) or startIndex
# making
< 0 or initSpeed < 0 or this bound tighter later on.
initSpeed > maxSpeed or
3
not runway[startIndex]):
maxSpeed = len(runway)
return False
# {position i : set of
4
speeds for which we can
stop fromifposition
(startIndex
i} >= len(runway) or startIndex < 0 or initSpeed < 0 or initSpeed >
5memo = {}
# BasemaxSpeed
cases, we can or not runway[startIndex]):
stop when a position is
6 a spikereturn
not and speed is
False
zero.
for position in
7
range(len(runway)):
# {position i : set of speeds for which we can stop from position i}
if runway[position]:
memo[position] =
8
set([0])
memo = {}
# Outer loop to go over
positions from the last one
9the first#one
to Base cases, we can stop when a position is not a spike and speed is zero.
for position in
reversed(range(len(runwa
10
y))): for position in range(len(runway)):
# Skip positions which
contain spikes
11if not runway[position]:
if runway[position]:
continue
# For each position, go
12 all possible
over memo[position]
speeds = set([0])
for speed in range(1,
maxSpeed + 1):
13# Recurrence relation
is the same as in the
http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 10 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

14 # Outer loop to go over positions from the last one to the first one

15 for position in reversed(range(len(runway))):

16 # Skip positions which contain spikes

17 if not runway[position]:

18 continue

19 # For each position, go over all possible speeds

20 for speed in range(1, maxSpeed + 1):

21 # Recurrence relation is the same as in the recursive version.

22 for adjustedSpeed in [speed, speed - 1, speed + 1]:

23 if (position + adjustedSpeed in memo and

24 adjustedSpeed in memo[position + adjustedSpeed]):

25 memo[position].add(speed)

26 break

return initSpeed in memo[startIndex]

Step 6: Add memoization


Memoization is a technique that is closely associated with DP. It is used for
storing the results of expensive function calls and returning the cached result
when the same inputs occur again. Why are we adding memoization to our
recursion? We encounter the same subproblems which without memoization
are computed repeatedly. Those repetitions very often lead to exponential
time complexities.

In recursive solutions, adding memoization should feel straightforward. Let’s


see why. Remember that memoization is just a cache of the function results.
There are times when you want to deviate from this definition in order to
squeeze out some minor optimizations, but treating memoization as a

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 11 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

function result cache is the most intuitive way to implement it.

This means that you should:

1. Store your function result into your memory before every return
statement
2. Look up the memory for the function result before you start doing any
other computation

Here is the code from above with added memoization (added lines are
highlighted):

def
canStopRecursiveWithMe
mo(runway, initSpeed,
startIndex = 0, memo =
None):
1# Only defdonecanStopRecursiveWithMemo(runway,
the first time initSpeed, startIndex = 0, memo =
None):
to initialize the memo.
if memo == None:
2memo = {}
# Only
# First check if thedone
result the first time to initialize the memo.
exists in memo
3if startIndex in memo and
initSpeedif inmemo == None:
memo[startIndex]:
4return
memo[startIndex]memo = {}
[initSpeed]
5# negative base cases
need to go #first
First check if the result exists in memo
if (startIndex >=
6
len(runway) or startIndex
< 0 or if startIndex in memo and initSpeed in memo[startIndex]:
initSpeed < 0 or not
7
runway[startIndex]):
return memo[startIndex][initSpeed]
insertIntoMemo(memo,
startIndex,
8 initSpeed,
False)
# negative base cases need to go first
return False
9# base case
stopping condition
for a

if initSpeedif (startIndex
== 0: >= len(runway) or startIndex < 0 or
10 insertIntoMemo(memo,
startIndex, initSpeed,
True) initSpeed < 0 or not runway[startIndex]):
11 return True
# Try all possible paths
insertIntoMemo(memo,
for adjustedSpeed in startIndex, initSpeed, False)
12
[initSpeed, initSpeed - 1,
initSpeed + 1]:
# Recurrencereturn False
relation: If
13 can stop from any of
you
the subproblems,
# base case for a stopping condition
# you can also stop
14
from the main problem
if
if initSpeed == 0:
canStopRecursiveWithMe
mo(
runway,
http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 12 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

15 insertIntoMemo(memo, startIndex, initSpeed, True)

16 return True

17 # Try all possible paths

18 for adjustedSpeed in [initSpeed, initSpeed - 1, initSpeed + 1]:

19 # Recurrence relation: If you can stop from any of the subproblems,

20 # you can also stop from the main problem

21 if canStopRecursiveWithMemo(

22 runway, adjustedSpeed, startIndex + adjustedSpeed, memo):

23 insertIntoMemo(memo, startIndex, initSpeed, True)

24 return True

25 insertIntoMemo(memo, startIndex, initSpeed, False)

26 return False

In order to illustrate the effectiveness of memoization and different


approaches, let’s do some quick tests. I will stress test all three methods that
we have seen so far. Here is the set up:

1. I created a runway of length 1000 with spikes in random places (I chose


to have a probability of a spike being in any given spot to be 20%)
2. initSpeed = 30
3. I ran all functions 10 times and measured the average time of execution

Here are the results (in seconds):

Time (s)
canStopRecursive 10.239
canStopIterative 0.021
canStopRecursiveWithMemo 0.008

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 13 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

You can see that the pure recursive approach takes about 500x more time
than the iterative approach and about 1300x more time than the recursive
approach with memoization. Note that this discrepancy would grow rapidly
with the length of the runway. I encourage you to try running it yourself.

Step 7: Determine Time complexity


There are some simple rules that can make computing time complexity of a
dynamic programming problem much easier. Here are two steps that you
need to do:

1. Count the number of states – this will depend on the number of


changing parameters in your problem
2. Think about the work done per each state. In other words, if everything
else but one state has been computed, how much work do you have to
do to compute that last state

In our example problem, the number of states is |P| * |S|, where

P is the set of all positions (|P| indicates the number of elements in P)


S is the set of all speeds

The work done per each state is O(1) in this problem because, given all other
states, we simply have to look at 3 subproblems to determine the resulting
state.

As we noted in the code before, |S| is limited by length of the runway (|P|), so
we could say that the number of states is |P|^2 and because work done per
each state is O(1), then the total time complexity is O(|P|^2).

However, it seems that |S| can be further limited because if it were really |P| it
is very clear that stopping would not be possible because you would have to
jump the length of the entire runway on the first move.

So let’s see how we can put a tighter bound on |S|. Let’s call maximum speed

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 14 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

S. Assume that we’re starting from position 0. How quickly could we stop if
we were trying to stop as soon as possible and if we ignore potential spikes?

In the first iteration, we would have to come at least to the point (S-1), by
adjusting our speed at zero by -1. From there we would at a minimum go by (S-
2) steps forward, and so on.

For a runway of length L, the following has to hold:

=> (S-1) + (S-2) + (S-3) + ….+ 1 < L

=> S * (S – 1) / 2 < L

=> S^2 – S – 2L < 0

If you find roots of the above function, they will be:

r1 = 1/2 + sqrt(1/4 + 2L) and r2 = 1/2 – sqrt(1/4 + 2L)

We can write our inequality as:

(S – r1) * (S – r2) < 0

Considering that S – r2 > 0 for any S > 0 and L > 0, we need the following:

S – 1/2 – sqrt(1/4 + 2L) < 0

=> S < 1/2 + sqrt(1/4 + 2L)

That is the maximum speed that we could possibly have on a runway of a


length L. If we had a speed higher than that, we could not stop even

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 15 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

theoretically, irrespective of the position of the spikes.

That means that the total time complexity depends only on the length of the
runway L in the following form:

O(L * sqrt(L)) which is better than O(L^2)

O(L * sqrt(L)) is the upper bound on the time complexity

Awesome, you made it through!

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 16 of 17
Dynamic Programming: 7 Steps to Solve any DP Interview Problem 15/06/18, 1)50 PM

The 7 steps that we went through should give you a framework for
systematically solving any dynamic programming problem. I highly
recommend practicing this approach on a few more problems to perfect your
approach.

Here are some next steps that you can take:

1. Extend the sample problem by trying to find a path to a stopping point.


We solved a problem that tells you whether you can stop, but what if you
wanted to also know the steps to take in order to stop eventually along
the runway. How would you modify the existing implementation to do
that?
2. One thing that could be quite useful in solidifying your understanding of
memoization and understanding that it is just a function result cache is
reading about decorators in python or similar concepts in other
languages. Think about how they would allow you to implement
memoization in general for any function that you want to memoize.
3. Work on more DP problems by following the steps we went through. You
can always find a bunch of them online (ex. LeetCode or GeeksForGeeks).
As you practice, keep in mind one thing: Learn ideas, don’t learn
problems. The number of ideas is significantly smaller and it’s an easier
space to conquer which will also serve you much better.

When you feel like you’ve conquered these ideas, check out Refdash where
you are interviewed by a senior engineer and get a detailed feedback on your
coding, algorithms, and system design.

http://blog.refdash.com/dynamic-programming-tutorial-example/ Page 17 of 17

You might also like