Instant Access to Programming Interview Problems: Dynamic Programming (with solutions in Python) 1st Edition Leonardo Rossi ebook Full Chapters
Instant Access to Programming Interview Problems: Dynamic Programming (with solutions in Python) 1st Edition Leonardo Rossi ebook Full Chapters
com
https://textbookfull.com/product/programming-interview-
problems-dynamic-programming-with-solutions-in-python-1st-
edition-leonardo-rossi/
OR CLICK BUTTON
DOWNLOAD NOW
https://textbookfull.com/product/robust-adaptive-dynamic-
programming-1st-edition-hao-yu/
textboxfull.com
https://textbookfull.com/product/python-network-programming-cookbook-
kathiravelu/
textboxfull.com
https://textbookfull.com/product/python-gui-programming-cookbook-
meier/
textboxfull.com
https://textbookfull.com/product/abstract-dynamic-programming-second-
edition-dimitri-p-bertsekas/
textboxfull.com
1
Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
General advice for the interview . . . . . . . . . . . . . . . . . . . . . . . 6
1 The Fibonacci sequence . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Solution 1: brute force, 𝑂(2𝑛 ) time 7
Solution 2: dynamic programming, top-down 9
Solution 2: dynamic programming, bottom-up 11
2 Optimal stock market strategy . . . . . . . . . . . . . . . . . . . . . . . . 13
Solution 1: dynamic programming, top-down, 𝑂(𝑛) time 13
Solution 2: dynamic programming, bottom-up, 𝑂(𝑛) time 15
Variation: limited investment budget 16
Variation: limited number of transactions 17
3 Change-making . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
Clarification questions 20
Solution 1: dynamic programming, top-down, 𝑂(𝑛𝑣) time 20
Solution 2: dynamic programming, bottom-up, 𝑂(𝑛𝑣) time 25
Solution 3: dynamic programming + BFS, bottom-up, 𝑂(𝑛𝑣) time 26
Variant: count the number of ways to pay (permutations) 29
Solution: dynamic-programming, top-down, 𝑂(𝑛𝑣) 29
Variant: count the number of ways to pay (combinations) 32
Solution: dynamic-programming, top-down, 𝑂(𝑛𝑣) 32
4 Number of expressions with a target result . . . . . . . . . . . . . . . . . . 36
Clarification questions 36
Solution 1: brute-force, 𝑂(2𝑛 ) time 36
Solution 2: dynamic programming, top-down, 𝑂(𝑛𝑆) time 38
Solution 3: dynamic programming + BFS, bottom-up, 𝑂(𝑛𝑆) time 39
Unit tests 41
5 Partitioning a set into equal-sum parts . . . . . . . . . . . . . . . . . . . . 43
Clarification questions 43
Solution 1: dynamic programming, top-down, 𝑂(𝑛𝑆) time 43
6 Splitting a string without spaces into words . . . . . . . . . . . . . . . . . . 46
Clarification questions 46
Solution 1: dynamic programming, top-down, 𝑂(𝑛𝑤) time 46
Solution 2: dynamic programming + BFS/DFS, bottom-up, 𝑂(𝑛𝑤) time 48
7 The number of binary search trees . . . . . . . . . . . . . . . . . . . . . . 50
Solution 1: dynamic programming, top-down, 𝑂(𝑛2 ) time 50
8 The maximum-sum subarray . . . . . . . . . . . . . . . . . . . . . . . . 55
Clarification questions 55
Solution 1: dynamic programming, 𝑂(𝑛) time, 𝑂(𝑛) space 55
Solution 2: dynamic programming, 𝑂(𝑛) time, 𝑂(1) space 61
Unit tests 61
9 The maximum-product subarray . . . . . . . . . . . . . . . . . . . . . . . 63
Clarification questions 63
Solution 1: greedy, two-pass, 𝑂(𝑛) time 63
Contents 3
Preface
Over the last decade, many companies have adopted the FANG-style interview process for
software engineer positions: programming questions that involve an algorithm design step,
and often require competitive programming solving techniques.
The advantages and disadvantages of this style of interview questions are highly debated, and
outside the scope of this book. What is important is that the questions that are asked pose
serious challenges to candidates, thus require thorough preparation.
The class of problems that are by far the most challenging is dynamic programming. This is
due to a combination of dynamic programming being rarely used in day-to-day work, and the
difficulty of finding a solution in a short amount of time, when not having prior experience
with this method.
This book presents 25 dynamic programming problems that are most commonly encoun-
tered in interviews, and several of their variants. For each problem, multiple solutions are
given, including a gradual walkthrough that shows how one may reach the answer. The goal
is not to memorize solutions, but to understand how they work and learn the fundamental
techniques, such that new, previously unseen problems can be solved with ease.
The solutions are very detailed, showing example walkthrougs, verbal explanations of the
solutions, and many diagrams and drawings. These were designed in a way that helps both
verbal and visual learners grasp the material with ease. The code implementation usually
comes last, accompanied by unit tests and complexity/performance analysis.
A particular focus has been put on code clarity and readability: during the interview, we are
expected to write code as we would on the job. This means that the code must be tidy, with
well-named variables, short functions that do one thing, modularity, comments describing
why we do certain things etc. This is, sadly, unlike most other material on dynamic pro-
gramming that one may find online, in competitive programming resources, or even in well-
known algorithm design textbooks. In fact, the poor state of the material on this topic is the
main reason I wrote this book.
I hope that you find this book useful for preparing to get the job you wish for. Whether you
like the book or not, I would appreciate your feedback through a book review.
Good luck with your interviews!
6 Contents
The above code is correct but too slow due to redundancies. We can see this if we add logging
to the function:
import inspect
def stack_depth():
return len(inspect.getouterframes(inspect.currentframe())) 1
def fibonacci(n):
print("{indent}fibonacci({n}) called".format(
indent=" " * stack_depth(), n=n))
if n <= 2:
return 1
return fibonacci(n 1) + fibonacci(n 2)
fibonacci(6)
We changed the code to print the argument passed to the fibonacci function. The message
is indented by the call stack depth, so that we can see better which function call is causing
which subsequent calls. Running the above code prints:
fibonacci(6) called
fibonacci(5) called
fibonacci(4) called
fibonacci(3) called
fibonacci(2) called
fibonacci(1) called
fibonacci(2) called
fibonacci(3) called
fibonacci(2) called
fibonacci(1) called
fibonacci(4) called
8 1 The Fibonacci sequence
fibonacci(3) called
fibonacci(2) called
fibonacci(1) called
fibonacci(2) called
That’s a lot of calls! If we draw the call graph, we can see that it’s an almost full binary tree:
9
Notice that the height of the binary tree is n (in this case, 6). The tree is almost full, thus
it has 𝑂(2𝑛 ) nodes. Since each node represends a call of our function, our algorithm has
exponential complexity.
It does not make sense to compute, for example, the 4-th Fibonacci number twice, since it
does not change. We should compute it only once and cache the result.
Notice how only the first call to fibonacci(n) recurses. All subsequent calls return from
the cache the value that was previously computed.
This implementation has 𝑂(𝑛) time complexity, since exactly one function call is needed to
compute each number in the series.
10 1 The Fibonacci sequence
While the above code is correct, there are some code style issues:
We can avoid adding the global variable by using instead an attribute called cache that is
attached to the function:
def fibonacci(n):
if n <= 2:
return 1
if not hasattr(fibonacci, 'cache'):
fibonacci.cache = {}
if n not in fibonacci.cache:
fibonacci.cache[n] = fibonacci(n 1) + fibonacci(n 2)
return fibonacci.cache[n]
The advantage is that the cache variable is now owned by the function, so no external code
is needed anymore to initialize it. The disadvantage is that the code has become even more
complicated, thus harder to read and modify.
A better approach is to keep the original function simple, and wrap it with a decorator that
performs the caching:
def cached(f):
cache = {}
def worker(*args):
if args not in cache:
cache[args] = f(*args)
return cache[args]
return worker
@cached
def fibonacci(n):
if n <= 2:
return 1
return fibonacci(n 1) + fibonacci(n 2)
The good news is that Python 3 has built-in support for caching decorators, so there is no
need to roll your own:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 2:
11
return 1
return fibonacci(n 1) + fibonacci(n 2)
By default lru_cache is limited to 128 entries, with least-recently used entries evicted when
the size limit is hit. Passing maxsize=None to lru_cache ensures that there is no memory
limit and all values are cached. In practice, it might be preferrable to set a limit instead of
letting memory usage increase without bounds.
The code has 𝑂(𝑛) time complexity, as well as 𝑂(𝑛) space complexity. In practice the perfor-
mance is better than the recursive implementation, since there is no overhead due to extra
function calls.
The space complexity can be reduced to 𝑂(1) if we notice that we do not need to store the
entire sequence, just the last two numbers:
def fibonacci(n):
previous = 1
current = 1
for i in range(n 2):
next = current + previous
previous, current = current, next
return current
We have written an algorithm that starts from the smallest subproblem (the first two numbers
in the sequence), then expands the solution to reach the original problem (the n-th number
in the sequence). This approach is called bottom-up dynamic programming. By contrast,
the previous approach of solving the problem recursively starting from the top is called top-
down dynamic programming. Both approaches are equally valid; one or the other may be
more intuitive, depending on the problem.
12 1 The Fibonacci sequence
In the rest of the book, we will look at how we can apply dynamic programming to solving
non-trivial problems. In general, we will show both top-down and bottom-up solutions. We
will see that the top-down approach is often easier to understand and implement, however it
offers less optimization opportunities compared to bottom-up.
13
Knowing this, we can model the entire problem using a state machine. In our initial state,
14 2 Optimal stock market strategy
we have some amount of cash and no shares. In the final state, we have some other amount
of cash (ideally higher), and no shares. In between, we have state transitions:
Solving the original problem can be reduced to finding a chain of transitions through this
state machine, that yields the maximum profit.
Notice how our state during any day only depends on the state from the previous day. This is
excellent: we can express our problem using a simple recurrence relation, just as we did for
the Fibonacci sequence problem.
The structure of the solution using a recursive algorithm looks like this:
def max_profit(daily_price):
def get_best_profit(day, have_stock):
"""
Returns the best profit that can be obtained by the end of the day.
At the end of the day:
* if have_stock is True, the trader must own the stock;
* if have_stock is False, the trader must not own the stock.
"""
# TODO ...
# Final state: end of last day, no shares owned.
last_day = len(daily_price) 1
no_stock = False
return get_best_profit(last_day, no_stock)
Note that we defined a helper function get_best_profit which takes as parameters the
identifiers of a state: the day number and whether we own the stock or not at the end of the
day. We use get_best_profit to compute the profit for a specific state in the state machine.
Let’s now implement the helper using a recurrence relation. We need to consider the previous
states that can transition into the current state, and choose the best one:
@lru_cache(maxsize=None)
def get_best_profit(day, have_stock):
"""
15
Returns the best profit that can be obtained by the end of the day.
At the end of the day:
* if have_stock is True, the trader must own the stock;
* if have_stock is False, the trader must not own the stock.
"""
if day < 0:
if not have_stock:
# Initial state: no stock and no profit.
return 0
else:
# We are not allowed to have initial stock.
# Add a very large penalty to eliminate this option.
return float('inf')
price = daily_price[day]
if have_stock:
# We can reach this state by buying or holding.
strategy_buy = get_best_profit(day 1, False) price
strategy_hold = get_best_profit(day 1, True)
return max(strategy_buy, strategy_hold)
else:
# We can reach this state by selling or avoiding.
strategy_sell = get_best_profit(day 1, True) + price
strategy_avoid = get_best_profit(day 1, False)
return max(strategy_sell, strategy_avoid)
The first part of the helper implements the termination condition, i.e. handling the initial
state, while the second part implements the recurrence. To simplify the logic of the recur-
rence we allow selling on any day including the first, but we ensure that selling on the first
day would yield a negative profit, so it’s an option that cannot be chosen as optimal.
Both the time and space complexity of this solution are 𝑂(𝑛). Note that it is important to
cache the results of the helper function, otherwise the time complexity becomes exponential
instead of linear.
def max_profit(daily_price):
# Initial state: start from a reference cash amount.
# It can be any value.
# We use 0 and allow our cash to go below 0 if we need to buy a share.
cash_not_owning_share = 0
# High penalty for owning a stock initially:
# ensures this option is never chosen.
cash_owning_share = float('inf')
for price in daily_price:
# Transitions to the current day, owning the stock:
strategy_buy = cash_not_owning_share price
strategy_hold = cash_owning_share
# Transitions to the current day, not owning the stock:
strategy_sell = cash_owning_share + price
strategy_avoid = cash_not_owning_share
# Compute the new states.
cash_owning_share = max(strategy_buy, strategy_hold)
cash_not_owning_share = max(strategy_sell, strategy_avoid)
# The profit is the final cash amount, since we start from
# a reference of 0.
return cash_not_owning_share
At each step, we only need to store the profit corresponding to the two states of that day. This
is due to the state machine not having any transitions between non-consecutive days: we
could say that at any time, the state machine does not “remember” anything from the days
before yesterday.
The time complexity is 𝑂(𝑛), but the space complexity has been reduced to 𝑂(1), since we
only need to store the result for the previous day.
Any time the optimal cash amount in a given state goes below zero, we replace it with negative
infinity. This ensures that this path through the state machine will not be chosen. We only
have to do this for the states where we own stock. In the states where we do not own stock,
our cash amount never decreases from the previous day, so this check is not needed.
strategy_buy,
strategy_hold)
cash_not_owning_share = cash_not_owning_share_next
cash_owning_share = cash_owning_share_next
# We have multiple final states, depending on how many times we sold.
# The transaction limit may not have been reached.
# Choose the most profitable final state.
return max(cash_not_owning_share)
3 Change-making
Given a money amount and a list of coin denominations, provide the combination of coins
adding up to that amount, that uses the fewest coins.
Example 1: Pay amount 9 using coin denominations [1, 2, 5]. The combination having
the fewest coins is [5, 2, 2]. A suboptimal combination is [5, 1, 1, 1, 1]: it adds up
to 9, but is using 5 coins instead of 3, thus it cannot be the solution.
Example 2: Pay amount 12 using coin denominations [1, 6, 10]. The combination having
the fewest coins is [6, 6]. A suboptimal combination is [10, 1, 1].
Clarification questions
Q: Is it possible that the amount cannot be paid with the given coins?
A: Yes. For example, 5 cannot be paid with coins [2, 4]. In such a situation, return None.
However, we do not know which coin to choose first optimally. In this situation, we have
no other choice but try all possible options in brute-force style. For each coin, we subtract
its value from the amount, then solve by recurrence the subproblem—this leads to a candi-
date solution for each choice of the first coin. Once we are done, we compare the candidate
solutions and choose the one using the fewest coins.
Here is an example of how we would form the optimal change for amount 9, using coins [1,
2, 5]. We represent each amount as a node in a tree. Whenever we subtract a coin value
from that amount, we add an edge to a new node with a smaller value. Please mind that the
actual solution does not use trees, at least not explicitly: they are shown here only for clarity.
21
This diagram shows that for value 9, we have three options for choosing the first coin:
• We choose coin 1. We now have to solve the subproblem for value 9 − 1 = 8. Suppose
its optimal result is [5, 2, 1]. Then we add coin 1 to create the candidate solution
[5, 2, 1, 1] for 9.
• We choose coin 2. We now have to solve the subproblem for value 9 − 2 = 7. Suppose
the optimal result is [5, 2]. We add to it coin 2 to create the candidate solution [5,
2, 2] for 9.
• We choose coin 5. We now have to solve the subproblem for value 9 − 5 = 4. The
optimal result is [2, 2]. We add to it coin 5 to create the candidate solution [5, 2,
2] for 9.
Now that we are done solving the subproblems, we compare the candidate solutions and
choose the one using the fewest coins: [5, 2, 2].
For solving the subproblems, we use the same procedure. The only difference is that we
need to pay attention to two edge cases: the terminating condition (reaching amount 0), and
avoiding choices where we are paying too much (reaching negative amounts). To understand
these better, let’s take a look at the subproblems:
22 3 Change-making
This algorithm implements the recurrence relation as explained above. Notice how we handle
the edge cases at the beginning of the function, before making any recursive calls, to avoid
infinite recursion and to keep the recursion logic as simple as possible.
This implementation has exponential time complexity, since we have not implemented
caching yet. Let’s do that now.
Unfortunately, if we try to add caching simply adding the lru_cache decorator, we will have
a nasty surprise:
from functools import lru_cache
@lru_cache(maxsize=None)
def make_change(coins, amount):
...
This code throws the exception: TypeError: unhashable type: 'list'. This is caused by
24 3 Change-making
the inability to cache the argument coins. As a list, it is mutable, and the lru_cache deco-
rator rejects it. The decorator supports caching only arguments with immutable types, such
as numbers, strings or tuples. This is for a very good reason: to prevent bugs in case mutable
arguments are changed later (in case of lists, via append, del or changing its elements), which
would require invalidating the cache.
A joke circulating in software engineering circles says that there are only two hard problems
in computer science: cache invalidation, naming things, and off-by-one errors. To address
the former, the design of the lru_cache decorator takes the easy way out: it avoids having to
implement cache invalidation at all by only allowing immutable arguments.
Still, we need to add caching one way or another. We can work around the lru_cache limi-
tation if we notice that we do not actually need to cache the coins list—that is shared among
all subproblems. We only need to pass the remaining amount to be paid. So we write a helper
function that solves the subproblem, taking as argument only the amount. The coin list is
shared between invocations.
One way to implement this is to make the helper function nested inside the make_change
function, so that it has access to the coins argument of the outer function:
def make_change(coins, amount):
@lru_cache(maxsize=None)
def helper(amount):
...
return helper(amount)
Another way is to transform the make_change function into a method of a class, that stores
the list of coins in a class member. The helper could then be written as another method of
the class, ideally a private one. I think adding classes is overkill for what we have to do, so we
will not discuss this approach.
optimal_result = None
# Consider all the possible ways to choose the last coin.
for coin in coins:
# Solve a subproblem for the rest of the amount.
partial_result = helper(amount coin)
# Skip this coin if the payment failed:
if partial_result is None:
continue
candidate = partial_result + [coin]
# Check if the candidate solution is better than the
# optimal solution known so far, and update it if needed.
if (optimal_result is None or
len(candidate) < len(optimal_result)):
optimal_result = candidate
return optimal_result
return helper(amount)
This solution is iterative, which is an advantage compared to the top-down solution, since it
avoids the overheads of recursive calls. However, for certain denominations, it wastes time
by increasing the amount very slowly, in steps of 1 unit. For example, if coins = [100,
200, 500] (suppose we use bills), it does not make sense to advance in steps of 1.
In addition, a lot of space is wasted for amounts that cannot be paid. Let’s see if we can come
up with a better solution.
Notice how we are treating the (sub)problem space as a graph, which is explored in breadth-
first order (BFS). We start from the subproblem of forming the amount 0. From there, we
keep expanding using all the possible coin choices until we reach the required amount.
Let’s look at an example showing how the problem space exploration works for paying
amount 9 with coins [1, 2, 5]. We start with amount 0 and explore by adding a coin in all
possible ways. Here is the first level of the problem space graph:
The first level contains all amounts that can be paid using a single coin. After this step, we
have [1, 2, 5] in our BFS queue, which is exactly the list of nodes on the first level—hence
the name of breadth-first search.
To fill in the second level of the problem space graph, we continue the exploration a step
further for amounts 1, 2 and 5:
28 3 Change-making
The second level contains all possible amounts that can be paid with 2 coins. After this step,
we have [3, 6, 4, 7] in our BFS queue.
Notice that we discarded nodes corresponding to already known amounts, since these can
be paid with a similar or better solution (fewer coins). We have also discarded amounts that
exceed the value we have to pay (such as 10). This ensures that the graph size is bounded and
that it contains only valid/optimal nodes.
We still have not found a way to pay our target amount, 9. Let’s explore further, adding the
third level of the problem space graph:
Note that as we reached the target amount 9, we stopped drawing the rest of the level. As
it is on the third level of the tree, the amount can be paid with 3 coins. The combination is
29
2, 2 and 5, which can be found by walking the path from 0 to 9. This is the solution to the
problem.
Note that the exploration order was important: the breadth-first order guarantees that each
time we reach a new amount, the path we followed is the shortest possible in terms of num-
ber of coins used. Had we used instead another graph search algorithm, such as depth-first
search, the solution might not have been optimal.
From this list we do not see any particular rule, other than enumerating all the permutations
of the combinations of coins that add up to 6 (1 + 5, 2 + 2 + 2, 2 + 2 + 1 + 1, 2 + 1 +
1 + 1 + 1 and 1 + 1 + 1 + 1 + 1 +1). This is not very helpful.
Sillä aikaa tuli tämä outo otus lähemmäksi. Sillä oli kupeillaan
jotkin kummalliset laitteet, jotka ruiskivat vettä ja pitivät sen täytistä
lotinaa ja litkutusta.
— Tua eij jou kuvan tulija, mikä liek kehnoj mualimal lopu' enteitä!
— päätteli edelleen Karhunen, joka oli niitä paikallaanpysyjiä,
korpeen syntyneitä ja siellä kasvaneita.
— On, on tok; ja kum minä ens' kesänä ajan tähär rantaa' oikeir
rupeljmasjsiinalla, niin suatta nähä, jotta siinä Kutaj ja Virmaar
rantalaistev venneet jeäp niinku' seisomaa'.
Vaan sitä ne kehuvat sitä separatieriä, vai mikä hän lie oikein siltä
tieteelliseltä nimeltään se maitomylly. Kuuluvat tuolla naapuripitäjissä
jo pienikarjaisetkin sen hankkineen. Meilläkin tässä on jo kymmenen
lypsävää ensi talvena, tuumiskeli Tuavetti edelleen, niin jotta jokohan
tuota olisi mentävä vaan virran mukana ja ostettava se mylly?
Tämä ajatus vaivasi häntä niin, että piti kysäistä Tolopilta, joka
paineli siinä vieressä. Kun Toloppi seuraavan kerran viitakettaan
läppäämään rupesi, teki Tuavettikin saman asian ja tiedusteli:
— Vua yks kone meille oes tarpeen, siit' oes tuolle äet’väille isoj
apu. Ohan ne kääneet meilläi ompelukonneen kaapalla, vua eipä
hänt' ou tullunna otetuks. Sen saes vähittäesmaksulla, ja sillä kävis
tuo neolomine' er' topakast. Kuulhan nuo Tervaharjulle sennii
hankkinee'.
— Se on opittava!
— Kyllä käet tätä sittä jo suap pittee varmana asijana, vae mittee
sinä sanot, Emma? — kysyy se poika hiljaa.
No nyt? Mitäs tämä nyt on? Eikös se tuo tyttö juokse sen pojan
kaulaan ihan solkenaan, ja oikein näyttää hyppäävän ja painavan
huulensa toisen huulia vastaan? No, nyt se ei enää muista siitä
irrotakaan. Aijai, jos sattuisi joku tulemaan. Onko tämä nyt
soveliasta? Kyllä tännekin saattaa joku osata.
Our website is not just a platform for buying books, but a bridge
connecting readers to the timeless values of culture and wisdom. With
an elegant, user-friendly interface and an intelligent search system,
we are committed to providing a quick and convenient shopping
experience. Additionally, our special promotions and home delivery
services ensure that you save time and fully enjoy the joy of reading.
textbookfull.com