Fibonacci Sequence: Figure 1. The Recursive Tree Formula For The Fibonacci Sequence
Fibonacci Sequence: Figure 1. The Recursive Tree Formula For The Fibonacci Sequence
The Fibonacci Sequence is the sequence where an element is a sum of two previous elements.
Let’s denote 𝐹(𝑛) as a function to find 𝑛!" Fibonacci number, then the general equation will be
0, 𝑖𝑓 𝑛 = 0
𝐹(𝑛) = * 1, 𝑖𝑓 𝑛 = 1
𝐹 (𝑛 − 1) + 𝐹 (𝑛 − 2), 𝑜𝑡ℎ𝑒𝑟𝑤𝑖𝑠𝑒
To find 6!" Fibonacci number, find 5!" and 4!" Fibonacci numbers and sum them up.
To find 5!" Fibonacci number, find 4!" and 3#$ Fibonacci numbers and sum them up.
To find 4!" Fibonacci number, find 3#$ and 2%$ Fibonacci numbers and sum them up.
To find 3#$ Fibonacci number, find 2%$ and 1&! Fibonacci numbers and sum them up.
4
𝐹(3) = 𝐹(2) + 𝐹(1)
To find 2%$ Fibonacci number, find 1&! and 0 Fibonacci numbers and then sum them up.
𝐹(1) = 1
𝐹(0) = 0
5
Coin Change
Another example of the Dynamic Programming problem is the Coin Change Problem.
Given a target amount and coins of different denominations, find the minimum number of coins to
make the target amount.
From the above example, the minimum number of coins to make 7 is three. We can use two coins with
denomination 1 and one coin with denomination 5, so 7 = 5 + 1 + 1 (there is another option to choose
3 + 3 + 1).
Let’s denote 𝐹(𝑛) as a function to find the minimum number of coins to make 𝑛 amount, then the
general equation will be
𝐹(𝑛 − 5)
𝐹(𝑛) = 𝑚𝑖𝑛 <𝐹(𝑛 − 3)? + 1
𝐹 (𝑛 − 1)
0, 𝑛 = 0
𝐹(𝑛) = @
minD𝐹(𝑛 − 5), 𝐹(𝑛 − 3), 𝐹(𝑛 − 1)E + 1, 𝑜𝑡ℎ𝑒𝑟𝑤𝑖𝑠𝑒
To find the minimum number of coins to make 𝐹(7), we can choose one of the following options:
• use one coin with denomination 5 and remain with new 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 2, 𝐹(2)
• use one coin with denomination 3 and remain with new 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 4, 𝐹(4)
• use one coin with denomination 1 and remain with new 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 6, 𝐹(6)
6
𝐹(2) (𝑢𝑠𝑒 5)
𝐹(7) = 𝑚𝑖𝑛 * 𝐹(4)(𝑢𝑠𝑒 3) 4 + 1
𝐹(6) (𝑢𝑠𝑒 1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(2) = 𝑚𝑖𝑛 6 = + 1
𝐹(1) (𝑢𝑠𝑒 1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0) (𝑢𝑠𝑒 1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5
𝐹(4) = 𝑚𝑖𝑛 * 𝐹(1) 4 + 1
𝐹(3)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0) (𝑢𝑠𝑒 1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5
𝐹(3) = 𝑚𝑖𝑛 * 𝐹(0) 4 + 1
𝐹(2)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(2) = 𝑚𝑖𝑛 6 = + 1
𝐹(1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
𝐹(1)
𝐹(6) = 𝑚𝑖𝑛 *𝐹(3)4 + 1
𝐹(5)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
7
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5
𝐹(3) = 𝑚𝑖𝑛 * 𝐹(0) 4 + 1
𝐹(2)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(2) = 𝑚𝑖𝑛 6 = + 1
𝐹(1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
𝐹(0)
𝐹(5) = 𝑚𝑖𝑛 *𝐹(2)4 + 1
𝐹(4)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(2) = 𝑚𝑖𝑛 6 = + 1
𝐹(1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5
𝐹(4) = 𝑚𝑖𝑛 * 𝐹(1) 4 + 1
𝐹(3)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5
𝐹(3) = 𝑚𝑖𝑛 * 𝐹(0) 4 + 1
𝐹(2)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(2) = 𝑚𝑖𝑛 6 = + 1
𝐹(1)
𝑐𝑎𝑛′𝑡 𝑢𝑠𝑒 5 𝑜𝑟 3
𝐹(1) = 𝑚𝑖𝑛 6 = + 1
𝐹(0)
𝐹(0) = 0
8
Figure 4. The recursive tree
for the Coin Change Problem.
9
Unique Paths
Given a 2D grid, find the total number of unique paths from the top-left (0,0) corner to the bottom-
right corner (𝑛 − 1, 𝑚 − 1), where 𝑛 is the number of rows and 𝑚 is the number of columns.
Let’s denote 𝐹(𝑥, 𝑦) as a function to find the total number of unique paths to reach a position (𝑥, 𝑦),
then the general equation will be
1, 𝑥 = 0 𝑎𝑛𝑑 𝑦 = 0
𝐹(𝑥, 𝑦) = @
𝐹 (𝑥 − 1, 𝑦) + 𝐹 (𝑥, 𝑦 − 1), 𝑜𝑡ℎ𝑒𝑟𝑤𝑖𝑠𝑒
For 𝑛 = 3 and 𝑚 = 3,
𝐹(0,2) = 𝐹(0,1)
10
𝐹(0,1) = 𝐹(0,0)
𝐹(0, 1) = 𝐹(0, 0)
𝐹(1, 0) = 𝐹(0, 0)
𝐹(2, 0) = 𝐹(1, 0)
𝐹(1, 0) = 𝐹(0, 0)
𝐹(0,0) = 1
From the recursive trees, we can see that the problem in the solutions above is the recalculation of the
same sub-problems (states) multiple times, which leads to unnecessary extra work.
11
How does Dynamic Programming help us to solve this problem?
Dynamic Programming breaks a problem into sub-problems (states) and finds the most optimal
solutions for all sub-problems. To avoid repetitive calculations, it caches already calculated solutions.
Then based on solutions for sub-problems, it builds the solution for the general problem.
12
How to solve Dynamic Programming Problems
Dynamic Programming problems can be categorized into optimization and combinatorial problems.
Optimization problems require finding the most optimal solution, such as the
minimum/maximum/shortest/longest answer, while combinatorial problems require finding a number
of ways/the probability of performing some operation.
In Computer Programming, there are two methods to solve Dynamic Programming problems:
• Top-Down (recursive)
• Bottom-Up (iterative)
Top-Down
The top-down approach solves a problem recursively, going from a target state to a base case and
breaking the problem into sub-problems (states). Then it finds an optimal solution for every sub-
problem and stores a result for reuse in the future.
13
How to come up with a recursive solution
Perform the following steps to understand how to come up with a recursive solution.
𝐹(𝑜𝑝𝑡𝑖𝑜𝑛 1)
𝐹(𝑛) → 𝐹(𝑜𝑝𝑡𝑖𝑜𝑛 1)
…
𝐹(𝑜𝑝𝑡𝑖𝑜𝑛 𝑘)
Fibonacci Sequence
𝐹(𝑙𝑎𝑠𝑡 𝑡𝑒𝑟𝑚)
𝐹(𝑐𝑢𝑟𝑟𝑒𝑛𝑡 𝑡𝑒𝑟𝑚) →
𝐹(𝑜𝑛𝑒 𝑏𝑒𝑓𝑜𝑟𝑒 𝑡ℎ𝑒 𝑙𝑎𝑠𝑡 𝑡𝑒𝑟𝑚)
Unique Paths
𝐹(𝑚𝑜𝑣𝑒 𝑢𝑝)
𝐹(𝑥, 𝑦) →
𝐹(𝑚𝑜𝑣𝑒 𝑙𝑒𝑓𝑡)
There are two options (directions) to choose to reach the target position:
• move up
• move left
14
Step 2. Reduce Target
Choosing every option, we reduce a target to a particular value, and we call a recursive function
again with the new updated target.
Fibonacci Sequence
𝐹(𝑛 − 1)
𝐹(𝑛) →
𝐹(𝑛 − 2)
• Choosing the last term 𝐹(𝑛 − 1) reduces the problem to find (𝑛 − 1)!" term.
• Choosing the one before the last term 𝐹(𝑛 − 2) reduces the problem to find (𝑛 − 2)!" term.
𝐹(1)
𝐹(2) →
𝐹(0)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 5)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡) → 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 3)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 1)
• Choosing the coin with denomination 5 reduces the problem to make the new target amount of
𝑡𝑎𝑟𝑔𝑒𝑡 − 5.
• Choosing the coin with denomination 3 reduces the problem to make the new target amount of
𝑡𝑎𝑟𝑔𝑒𝑡 − 3.
• Choosing the coin with denomination 1 reduces the problem to make the new target amount of
𝑡𝑎𝑟𝑔𝑒𝑡 − 1.
𝐹(11 − 5)
𝐹(11) → 𝐹 (11 − 3)
𝐹(11 − 1)
𝐹(6)
𝐹(11) → 𝐹 (8)
𝐹(10)
Unique Paths
15
𝐹(𝑥 − 1, 𝑦)
𝐹(𝑥, 𝑦) →
𝐹(𝑥, 𝑦 − 1)
• Choosing the option to move up reduces the problem to find the number of unique paths to
reach (𝑥 − 1, 𝑦).
• Choosing the option to move left reduces the problem to find the number of unique paths to
reach (𝑥, 𝑦 − 1).
𝐹(1,2)
𝐹(2,2) →
𝐹(2,1)
Fibonacci Sequence
Choose the option that leads to the most optimal solution and add one to use one coin for the current
state.
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 5)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡) = 𝑚𝑖𝑛 <𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 3)? + 1
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 1)
Unique Paths
16
Step 4. Base Case
When we reach a base case, we need to quit a recursive function. The base case can be a base state
or case when a sub-problem is no longer valid (in this case, when it returns the worst result).
Fibonacci Sequence
We already know that the first two Fibonacci numbers are 0 and 1 respectively.
𝐹(1) = 1
𝐹(0) = 0
1 int F(n) {
2 if (n == 0) {
3 // base case
4 }
5 if (n == 1) {
6 // base case
7 }
8 }
Coin Change
When 𝑡𝑎𝑟𝑔𝑒𝑡 equals 0 (we don’t need any coin to make 0) or less than 0, the function reaches the
base case.
𝐹(0) = 0
1 int F(target) {
2 if (target == 0) {
3 // base case
4 }
5 if (target < 0) {
6 // not valid
7 }
8 }
Unique Paths
When the position equals the destination cell or the position is out of bounds, the function reaches the
base case.
𝐹(0, 0) = 1
17
1 int F(x, y) {
2 if (x == 0 && y == 0) {
3 // base case
4 }
5 if (x < 0 || y < 0) {
6 // not valid
7 }
8 }
return result
1 int Fib(int n) {
2 if (n == 0) {
3 return 0; // step 4
4 }
5 if (n == 1) {
6 return 1; // step 4
7 }
8
9 int last = Fib(n-1); // step 1 & 2
10 int beforeLast = Fib(n-2); // step 1 & 2
11 int result = last + beforeLast; // step 3
12
13 return result; // step 5
14 }
18
7 if (coins[i] <= target) {
8 int substateSolution = CoinChange(target-coins[i], coins); // step 2
9 result = min(result, substateSolution+1); // step 3
10 }
11 }
12 return result; // step 5
13 }
The problem with these recursive solutions is the recalculation of sub-problems, which leads to slow
performance.
19
How to calculate the time complexity of a recursive solution
To calculate the time-complexity of a recursive solution, try to answer the following questions:
• How many times does a function call itself (𝑡)?
• How many times is a function being recursed (𝑘)?
Based on that, we can say that the time complexity of a plain recursive solution is exponential 𝑂(𝑡 ' ).
If we draw a recursive tree, we can find the time complexity by summing up the number of nodes in
the tree.
Fibonacci Sequence
To find 𝑛!" Fibonacci number, we need to sum up two previous Fibonacci numbers.
• 𝐹(𝑛 − 1)
• 𝐹(𝑛 − 2)
Then it’s being recursed 𝑛 times from 𝑛 to 0, where 𝑛 is the depth of the recursive tree for the
function.
Let’s calculate the time complexity of the function to see if the statement above is true.
Let’s denote 𝑇(𝑛) as a function to calculate the time complexity of the Fibonacci Sequence.
20
𝑇(𝑛) = 𝑇(𝑛 − 1) + 𝑇(𝑛 − 2) + 𝐶 ,
where 𝑇(𝑛 − 1) and 𝑇(𝑛 − 2) are the time complexities to find 𝑛 − 1!" Fibonacci and 𝑛 − 2!" Fibonacci
numbers respectively, and 𝐶 is the number of constant operations performed inside the recursive call.
𝑇(𝑛) = 2 × 𝑇(𝑛 − 1) + 𝐶
𝑇(𝑛 − 1) = 2 × 𝑇(𝑛 − 2) + 𝐶
𝑇(𝑛) = 2 × (2 × 𝑇(𝑛 − 2) + 𝐶) + 𝐶
𝑇(𝑛) = 22 × 𝑇(𝑛 − 2) + 3𝐶
𝑇(𝑛 − 2) = 2 × 𝑇(𝑛 − 3) + 𝐶
𝑇(𝑛) = 22 × (2 × 𝑇(𝑛 − 3) + 𝐶) + 3𝐶
𝑇(𝑛) = 23 × 𝑇(𝑛 − 3) + 7𝐶
𝑇(0) = 1
𝑛−𝑘 = 0
𝑛 = 𝑘
Using the idea that 𝐹(𝑛) and 𝑇(𝑛) are calculated in the same way, we can find a tighter upper bound
time complexity. Approximate value for 𝐹(𝑛) is 𝑂(𝜙 % ), where 𝜙 = (1 + √5)/2 ≈ 1.618.
Consequently,
21
Coin Change
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 1)
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 2)
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 3)
• …
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 𝑛)
So, the time complexity of the plain recursive solution is 𝑂(𝑛!(#)*! (,-.%! ).
Unique Paths
• 𝐹(𝑥 − 1, 𝑦)
• 𝐹(𝑥, 𝑦 − 1)
Then it’s being recursed 𝑛 + 𝑚 times, where 𝑛 is the number of rows and 𝑚 is the number of columns.
For every recursive call, we move one step up, or one step left. In other words, to reach leaf nodes, we
need to exhaust total rows and total columns, meaning that the depth of our recursive tree will be 𝑛 +
𝑚.
22
Figure 9. The recursive tree for Unique Paths.
So, the time complexity of the plain recursive solution is 𝑂(2%/, ) ≈ 𝑂(2% )..
All the recursive solutions above lead to exponential time complexity due to overlapping sub-
problems.
Memoization
A recursive solution has overlapping sub-problems that are being calculated several times. To avoid
repetitive calculations, we need to store already calculated solutions for sub-problems. Next time, we
will also need to use stored values instead of a recalculation. This technique is called memoization.
To understand how sub-problem solutions are stored, we need to know the keys for the memoization
table. According to The Theory of Dynamic Programming, we have state parameters (or state
variables) that determine each state. These parameters transform each state.
23
“We have a physical system whose state at any time is determined by a set of quantities which we
call state parameters, or state variables (The Theory of Dynamic Programming, Richard Bellman).”
“These decisions are equivalent to transformations of the state variables, the choice of a decision
being identical with the choice of a transformation. The outcome of the preceding decisions is to
be used to guide the choice of future ones, with the purpose of the whole process of maximizing
some function of the parameters describing the final state (The Theory of Dynamic Programming,
Richard Bellman).”
The statements above give an idea to use state parameters to store information about a particular
state’s decision.
Fibonacci Sequence
1 int Fib(int n) {
2 if (n == 0) {
3 return 0; // base case
4 }
5 if (n == 1) {
6 return 1; // base case
7 }
8 return Fib(n-1) + Fib(n-2);
9 }
For the Fibonacci Sequence, each state 𝐹𝑖𝑏(𝑖𝑛𝑡 𝑛) has the term number 𝑛 as a parameter,
transforming each state. Based on this information, we can use the parameter 𝑛 as the key for the
memoization table.
1 int Fib(int n) {
2 if (n == 0) {
3 return 0; // base case
4 }
5 if (n == 1) {
6 return 1; // base case
7 }
8 if (memo[n] != -1) { // check if a state was already calculated
24
9 return memo[n];
10 }
11 memo[n] = Fib(n-1) + Fib(n-2); // store calculations
12 return memo[n];
13 }
Coin Change
The parameter 𝑡𝑎𝑟𝑔𝑒𝑡 transforms each state, and we can use it as the key for the memoization table.
Consequently,
25
18 memo[target] = result;
19
20 return result;
21 }
Unique Paths
There are two state parameters 𝑥 and 𝑦, which we can use as the keys for the memoization table.
To calculate the time-complexity of a recursive solution with memoization, try to answer the following
questions:
• How many states do we have in total? (𝑁)?
• How many times does a function call itself? (𝑡)?
26
The only difference is the fact that memoization will help us to avoid repetitive calculations for sub-
problems. As a result, the time complexity will be 𝑂(𝑁𝑡).
Fibonacci Sequence
1 int Fib(int n) {
2
3 /*base cases*/
4
5 if (memo[n] != -1) {
6 return memo[n]; // check if a state was already calculated
7 }
8 memo[n] = Fib(n-1) + Fib(n-2); // store calculations
9 return memo[n];
10 }
• 𝐹(𝑛 − 1)
• 𝐹(𝑛 − 2)
We have a total number of 𝑛 states. The time complexity of the recursive solution with memoization is
𝑂(𝑛).
Coin Change
27
19 }
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 1)
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 2)
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 3)
• ...
• 𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 − 𝑐𝑜𝑖𝑛 𝑛)
Here we have a total number of 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 states. The time complexity of the recursive solution
with memoization is 𝑂(𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 × 𝑛).
Unique Paths
• 𝐹(𝑥 − 1, 𝑦)
• 𝐹(𝑥, 𝑦 − 1)
We have a total of 𝑛𝑚 states. The time complexity of the recursive solution with memoization is 𝑂(𝑛𝑚).
28
Bottom-Up
The bottom-up approach solves a problem iteratively, going from a base case towards a target state.
The bottom-up method finds an optimal solution for each sub-problem and stores a result for each
sub-problem for later use. There are two ways of solving problems iteratively: backward style and
forward style.
Backward Style
In the backward style iteration, we calculate a solution for the current state based on solutions of the
previous states.
Fibonacci Sequence
Forward Style
In the forward style iteration, we calculate a solution for the next state based on solutions of the
current state.
Fibonacci Sequence
29
How to come up with an iterative solution
Perform the following steps to understand how to come up with an iterative solution.
The base case can be a state we already know or can calculate the answer.
Fibonacci Sequence
We already know that the first two Fibonacci numbers are 0 and 1 respectively.
𝑡𝑎𝑏𝑙𝑒(0) = 0
𝑡𝑎𝑏𝑙𝑒(1) = 1
Coin Change
𝑡𝑎𝑏𝑙𝑒(0) = 0
Unique Paths
We are using zero-indexed positions, and we have only one path to reach (0, 0) position.
𝑡𝑎𝑏𝑙𝑒(0, 0) = 1
Fibonacci Sequence
𝑡𝑎𝑏𝑙𝑒(0) = 0
𝑡𝑎𝑏𝑙𝑒(1) = 1
𝑡𝑎𝑏𝑙𝑒(2) = . ..
𝑡𝑎𝑏𝑙𝑒(𝑛) = . ..
30
Coin Change
𝑡𝑎𝑏𝑙𝑒(0) = 0
𝑡𝑎𝑏𝑙𝑒(1) = 1
𝑡𝑎𝑏𝑙𝑒(2) = . ..
𝑡𝑎𝑏𝑙𝑒(𝑡𝑎𝑟𝑔𝑒𝑡) = . ..
Unique Paths
𝑡𝑎𝑏𝑙𝑒(0, 0) = 1
𝑡𝑎𝑏𝑙𝑒(0, 1) = . ..
𝑡𝑎𝑏𝑙𝑒(0, 2) = . ..
𝑡𝑎𝑏𝑙𝑒(1, 1) = . ..
𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦) = . ..
Fibonacci Sequence
𝑡𝑎𝑏𝑙𝑒(𝑛 − 1)
𝑡𝑎𝑏𝑙𝑒(𝑛) →
𝑡𝑎𝑏𝑙𝑒(𝑛 − 2)
To obtain 𝑛!" term there are two options to come from to the current state:
• the last term 𝑛 − 1
• one before the last term 𝑛 − 2
Coin Change
31
To make the 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡, there are three options to come from to the current state:
• using a coin with denomination 5
• using a coin with denomination 3
• using a coin with denomination 1
We are starting from the base case to apply a coin with denomination 𝐾 check if the current target is
greater or equal to the coin denomination.
Unique Paths
𝑡𝑎𝑏𝑙𝑒(𝑥 − 1, 𝑦)
𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦) →
𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦 − 1)
To reach the target, there are two options (directions) to come from to the current state:
• from top 𝑥 − 1
• from left 𝑦 − 1
Fibonacci Sequence
Coin Change
Find an optimal solution without the current state and add one coin for the current state.
Unique Paths
32
Step 5. Return Result
After reaching a target state, return an answer for the target state.
Fibonacci Sequence
𝑟𝑒𝑡𝑢𝑟𝑛 𝑡𝑎𝑏𝑙𝑒(𝑛)
Coin Change
𝑟𝑒𝑡𝑢𝑟𝑛 𝑡𝑎𝑏𝑙𝑒(𝑡𝑎𝑟𝑔𝑒𝑡)
Unique Paths
𝑟𝑒𝑡𝑢𝑟𝑛 𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦)
1 int Fib(int n) {
2 vector<int> table(n+1);
3
4 table[0] = 0; // step 1
5 table[1] = 1; // step 1
6
7 for (int i = 2; i <= n; ++i) { // step 2
8 int last = table[i-1]; // step 3
9 int beforeLast = table[i-2]; // step 3
8 table[i] = last + beforeLast; // step 4
9 }
10
11 return table[n]; // step 5
12 }
33
10 }
11 }
12 }
13
14 return table[target] == inf ? -1 : table[target]; // step 5
15 }
To calculate the time complexity of the iterative solution, we need to count how many operations we
are performing inside the function.
Fibonacci Sequence
To find 𝑛!" Fibonacci number, we need to iterate over all Fibonacci numbers till 𝑛!" term. Inside the
single loop, we are performing constant operations 𝑂(1). Time complexity is 𝑂(𝑛).
1 int Fib(int n) {
2
3 /*code*/
4
5 for (int i = 2; i <= n; ++i) {
34
6 table[i] = table[i-1] + table[i-2];
7 }
8
9 return table[n];
10 }
Coin Change
To find the minimum number of coins to make 𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡, we need to iterate over all options for
every target. Inside the nested loop, we are performing constant operations 𝑂(1). The time complexity
is 𝑂(𝑡𝑎𝑟𝑔𝑒𝑡 𝑎𝑚𝑜𝑢𝑛𝑡 ∗ 𝑛), where 𝑛 is the number of coins.
Unique Paths
To reach the target position, we need to iterate over all positions and sum up unique paths. The time
complexity is 𝑂(𝑛𝑚).
35
13 }
14 }
15 }
16
17 return table[x-1][y-1]; // return result
18 }
36
Top-Down vs. Bottom-Up
The top-down (recursive) solution is easy to develop, though it needs extra space for a recursive call
stack. The bottom-up (iterative) solution is not intuitive to come up with, but it avoids extra space.
Both methods have base cases to start and to finish operations. After finding a recursive solution, we
can use base cases of a recursive solution for an iterative solution.
1 int topDown(int n) {
2 if (base case) {
3 return result; // base case
4 }
5 }
1 int bottomUp(int n) {
2 table[base case] = result;
3 }
Fibonacci Sequence
1 int topDown(int n) {
2 if (n == 0) {
3 return 0; // base case
4 }
5 if (n == 1) {
6 return 1; // base case
7 }
8
9 ...
10 }
1 int bottomUp(int n) {
2 ...
37
3
4 table[0] = 0; // base case
5 table[1] = 1; // base case
6
7 ...
8 }
Coin Change
1 int topDown(int n) {
2 if (target == 0) {
3 return 0; // base case
4 }
5
6 ...
7 }
1 int bottomUp(int n) {
2 ...
3
4 table[0] = 0; // base case
5
6 ...
7 }
Unique Paths
1 int topDown(int n) {
2 if (x == 0 && y == 0) {
3 return 1; // base case
4 }
5 if (x < 0 || y < 0) {
6 return 0; // not valid sub-problem
7 }
8
9 ...
10 }
1 int bottomUp(int n) {
2 ...
3
4 table[0][0] = 1; // base case
5
38
6 ...
7 }
In a recursive solution, there is a memoization table to store sub-state solutions. We can use the same
table for an iterative solution as well to store sub-state solutions.
Fibonacci Sequence
1 int bottomUp(int n) {
2 ...
3
4 for (...) {
5 table[n] = /* result */
6 }
7
8 return table[n];
9 }
Coin Change
39
8 ...
9
10 memo[target] = /* result */
11
12 return result;
13 }
Unique Paths
40
16 }
17
18 return table[x-1][y-1];
19 }
Now it’s time to find out how to replace recursive calls with iterative calls. Let’s recall the statement
from the paper.
“We have a physical system whose state at any time is determined by a set of quantities which we
call state parameters, or state variables (The Theory of Dynamic Programming, Richard Bellman).”
Consequently, we can use state parameters used in recursive calls for iterative calls as well.
Fibonacci Sequence
1 int bottomUp(int n) {
2 ...
3
4 for (int i = 2; i <= n; ++i) { // n - transforming state parameter
5 table[i] = /* result */
6 }
7
8 return table[n];
9 }
Coin Change
41
𝑡𝑎𝑟𝑔𝑒𝑡 is a state parameter transforming each state.
Unique Paths
42
9 }
Fibonacci Sequence
𝑡𝑎𝑏𝑙𝑒(𝑛 − 1)
𝑡𝑎𝑏𝑙𝑒(𝑛) →
𝑡𝑎𝑏𝑙𝑒(𝑛 − 2)
To obtain 𝑛!" term, there are two options to come from to the current state:
• the last term
• one before the last term
1 int bottomUp(int n) {
2 ...
43
3
4 for (int i = 2; i <= n; ++i) {
5 table[i] = table[i-1] + table[i-2];. // Iterate over all options
6 }
7
8 return table[n];
9 }
Coin Change
To make the target amount, there are three options to come from to the current state:
• using the coin with denomination 5
• using the coin with denomination 3
• using the coin with denomination 1
44
11
12 return table[target] == inf ? -1 : table[target];
13 }
Unique Paths
𝑡𝑎𝑏𝑙𝑒(𝑥 − 1, 𝑦)
𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦) →
𝑡𝑎𝑏𝑙𝑒(𝑥, 𝑦 − 1)
To reach the target, there are two options (directions) to come from to the current state:
• from top 𝑥 − 1
• from left 𝑦 − 1
45
Step 5. Choose an Optimal Solution
Find an optimal solution (maximum, minimum, shortest, longest, etc.) among all states and add a
value for the current state.
For every state, we need to choose the most optimal sub-state solution and add the value for the
current state.
Fibonacci Sequence
1 int bottomUp(int n) {
2 ...
3
4 for (int i = 2; i <= n; ++i) {
5 table[i] = table[i-1] + table[i-2];. // Sum up two previous values
6 }
7
8 return table[n];
9 }
Coin Change
Find an optimal solution without the current state and add one coin for the current state.
46
3
4 for (int i = 0; i < coins.size(); ++i) { // Iterate over all options.
5 if (coins[i] <= target) {
6 result = min(result, CoinChange(target - coins[i], coins) + 1); // Find
an optimal solution
7 }
8 }
9
10 memo[target] = result;
11
12 return result;
13 }
Unique Paths
47
2 ...
3
4 for (int i = 0; i < x; ++i) { // x - transforming state parameter
5 for (int j = 0; j < y; ++j) { // y - transforming state parameter
6 if (i > 0 && j > 0) {
7 table[i][j] = table[i-1][j] + table[i][j-1]; // Sum up both options
to find a total path.
8 } else if (i > 0) {
9 table[i][j] = table[i-1][j]; // Sum up both options to find a total
path.
10 } else if (j > 0) {
11 table[i][j] = table[i][j-1]; // Sum up both options to find a total
path.
12 }
13 }
14 }
15
16 return table[x-1][y-1];
17 }
Fibonacci Sequence
return table[n]
Coin Change
return table[target]
Unique Paths
return table[x-1][y-1]
48
Practicing Common Dynamic Programming Problems
Find the minimum cost required to reach the top stair from the bottom stair, climbing either 1 or 2
stairs each time. To climb, you need to pay the cost.
Intuition
For every stair, we choose an option that leads to the minimum cost among all possible options
before the current stair, and we add the cost for the current stair.
Top-Down
To reach the top stair, there are two options to choose from:
• climb 1 stair
• climb 2 stairs
𝐹(𝑐𝑙𝑖𝑚𝑏 1 𝑠𝑡𝑎𝑖𝑟)
𝐹(𝑐𝑢𝑟𝑟𝑒𝑛𝑡 𝑠𝑡𝑎𝑖𝑟) →
𝐹(𝑐𝑙𝑖𝑚𝑏 2 𝑠𝑡𝑎𝑖𝑟𝑠)
𝐹(𝑛 − 1)
𝐹(𝑛) →
𝐹(𝑛 − 2)
• Climbing 1 stair reduces the problem to find the minimum cost required to reach 𝑛 − 1!" stair.
• Climbing 2 stairs reduces the problem to find the minimum cost required to reach 𝑛 − 2!" stair.
49
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
𝐹(𝑛 − 1)
𝐹(𝑛) = 𝑚𝑖𝑛 ` a + 𝑐𝑜𝑠𝑡
𝐹(𝑛 − 2)
We choose the minimum cost among all sub-problem solutions before the current stair, and we add
the cost for the current stair.
If you start to climb from 0 floor, you will pay 𝑐𝑜𝑠𝑡[0], and if you start to climb from 1st floor, you will
pay 𝑐𝑜𝑠𝑡[1].
if (n == 0) {
return cost[0]; // base case
}
if (n == 1) {
return cost[1]; // base case
}
Step 5. Memoization
We use transforming state variables for memorization.
if (memo[n] != -1) {
return memo[n];
}
50
return result
Solution
Analysis
Time-Complexity: 𝑂(𝑛)
We are calling the function two times and have a total of 𝑛 states, where 𝑛 is the number of stairs.
Space-Complexity: 𝑂(𝑛)
Bottom-Up
Base cases are 𝑛 = 0 and 𝑛 = 1 because we can start climbing from stairs 0 and 1 and know the costs
for these stairs.
51
Step 2. Move Toward a Target State
Starting from a base case, we move toward a target state.
We already know the solution for the base cases. To find solutions for other stairs, we iterate till 𝑛.
We choose an option with the minimum cost before the current stair, and we add the cost for the
current stair.
return dp[n]
52
Solution
Analysis
Time-Complexity: 𝑂(𝑛)
Inside the single loop, we are performing constant 𝑂(1) operations 𝑛 times, where 𝑛 is the number of
stairs.
Space-Complexity: 𝑂(𝑛)
53
Minimum Path Sum
Given a 2D grid with non-negative numbers, find the path from the top-left corner to the bottom-
right corner with the minimum sum of all numbers along the path.
Intuition
For every position, we choose the path with the minimum sum among all paths before the
current position, and we add the value for the current position.
Top-Down
To reach the target position, there are two options to choose from:
• move down 𝑖 + 1
• move right 𝑗 + 1
𝐹(𝑚𝑜𝑣𝑒 𝑑𝑜𝑤𝑛)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛) →
𝐹(𝑚𝑜𝑣𝑒 𝑟𝑖𝑔ℎ𝑡)
𝐹(𝑖 + 1, 𝑗)
𝐹(𝑡𝑎𝑟𝑔𝑒𝑡 𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛) →
𝐹(𝑖, 𝑗 + 1)
54
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
𝐹(𝑖 + 1, 𝑗)
𝐹(𝑛) = 𝑚𝑖𝑛 ` a + 𝑣𝑎𝑙𝑢𝑒
𝐹(𝑖, 𝑗 + 1)
We choose the minimum sum among sub-problem solutions, and we add the value for the current
position.
If the position reaches the target position, there is no need to move further; we return the value for the
target position. If the position is out of bounds of the grid, then the problem becomes invalid, we
return the worst result.
// invalid position
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size()) {
return INT_MAX;
}
Step 5. Memoization
We use transforming state variables for memorization.
Indices 𝑖 and 𝑗 are state variables transforming each state. We can use them for memoization.
if (memo[i][j] != -1) {
return memo[i][j];
}
55
Step 6. Return Result
Return a result of a recursive function.
return memo[i][j]
Solution
// invalid position
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size()) {
return INT_MAX;
}
if (memo[i][j] != -1) {
return memo[i][j];
}
memo[i][j] = result;
return result;
}
Analysis
Time-Complexity: 𝑂(𝑛𝑚)
We are calling the function two times and have a total of 𝑛𝑚 states, where 𝑛 is the number of rows and
𝑚 is the number of columns.
Space-Complexity: 𝑂(𝑛𝑚)
56
Bottom-Up
For base cases, we can calculate the values for the first row ignoring other rows. This means that we
have only one option to choose from, the same row previous column. Similarly, we can calculate the
values for the first column ignoring other columns. In this case, we can choose only the same column
previous row.
We iterate over all positions moving towards the target position (𝑛 − 1, 𝑚 − 1).
We can come to the current position from the top (𝑖 − 1, 𝑗) and the left (𝑖, 𝑗 − 1) side positions.
57
}
We choose an option with the minimum sum and add the value for the current position.
return grid[n-1][m-1]
Solution
int n = (int)grid.size();
int m = (int)grid[0].size();
58
grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j];
}
}
return grid[n-1][m-1];
}
Analysis
Time-Complexity: 𝑂(𝑛𝑚)
Inside the nested loop, we are performing constant 𝑂(1) operations 𝑛𝑚 times, where 𝑛 is the number
of rows and 𝑚 is the number of columns.
Space-Complexity: 𝑂(𝑛𝑚)
59
Climbing Stairs
Find the number of distinct ways to reach the top stair from the bottom stair, climbing either 1 or 2
stairs each time.
Intuition
For every stair, we sum all possible ways to reach the current stair. The solution for the top stair
will be the answer.
Top-Down
To reach the top stair, there are two options to choose from:
• climb 1 stair
• climb 2 stairs
𝐹(𝑐𝑙𝑖𝑚𝑏 1 𝑠𝑡𝑎𝑖𝑟)
𝐹(𝑐𝑢𝑟𝑟𝑒𝑛𝑡 𝑠𝑡𝑎𝑖𝑟) →
𝐹(𝑐𝑙𝑖𝑚𝑏 2 𝑠𝑡𝑎𝑖𝑟𝑠)
𝐹(𝑛 − 1)
𝐹(𝑛) →
𝐹(𝑛 − 2)
• Climbing 1 stair reduces the problem to find the number of distinct ways to reach the 𝑛 − 1!"
stair.
• Climbing 2 stairs reduces the problem to find the number of distinct ways to reach the 𝑛 − 2!"
stair.
60
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
if (n < 2) {
return 1; // base case
}
Step 5. Memoization
We use transforming state variables for memorization.
if (memo[n] != -1) {
return memo[n];
}
return memo[n]
Solution
61
if (n < 2) {
return 1; // base case
}
if (memo[n] != -1) {
return memo[n];
}
return memo[n];
}
Analysis
Time-Complexity: 𝑂(𝑛)
We are calling the function two times and have a total of 𝑛 states, where 𝑛 is the number of stairs.
Space-Complexity: 𝑂(𝑛)
Bottom-Up
We already know the answer for the base cases. We iterate over other floors till 𝑛!" floor to find out
solutions for the rest of the floors.
62
dp[0] = 1; // base case
dp[1] = 1; // base case
Return dp[n]
Solution
int bottomUp(int n) {
if(n < 2) {
63
return 1; // early exit
}
int dp[n+1];
return dp[n];
}
Analysis
Time-Complexity: 𝑂(𝑛)
Inside the single loop, we are performing constant 𝑂(1) operations 𝑛 times.
Space-Complexity: 𝑂(𝑛)
Note
Some questions point out the number of repetitions. In that case, add one more loop to simulate
every repetition.
64
Burst Balloons
Given balloons with values, after the burst of 𝑖!" balloon you will get 𝑏𝑎𝑙𝑙𝑜𝑜𝑛𝑠[𝑖 − 1] ∗ 𝑏𝑎𝑙𝑙𝑜𝑜𝑛𝑠[𝑖] ∗
𝑏𝑎𝑙𝑙𝑜𝑜𝑛𝑠[𝑖 + 1] coins. After each burst, balloons 𝑖 − 1 and 𝑖 + 1 become adjacent. Find the maximum
number of coins you can get by bursting balloons.
Intuition
We iterate over every interval, then for every balloon in the interval, we check the possible
maximum number of coins we can get if we burst this balloon last.
Top-Down
We have several options to burst balloons. For every interval in the list of balloons, we can try to burst
balloons at every index last to get the maximum number of coins. Therefore, we need to iterate
through indices of balloons.
// starting from i to j
for (int k = i; k <= j; ++k) {
/* code */
}
Every time we choose some index to burst a balloon, we split the array of balloons into two parts at
that index (excluding the index). Then we call the recursive function again with the new intervals.
𝐹(𝑖, 𝑘 − 1)
𝐹(𝑛) → 𝑝𝑟𝑜𝑑𝑢𝑐𝑡(𝑘)
𝐹(𝑘 + 1, 𝑗)
Choosing index 𝑘:
• The first half will be from 𝑖 till 𝑘 − 1.
• The second half will be from 𝑘 + 1 till 𝑗.
65
𝑡𝑜𝑝𝐷𝑜𝑤𝑛(𝑛𝑢𝑚𝑠, 𝑖, 𝑘 − 1, 𝑚𝑒𝑚𝑜)
𝑟𝑒𝑠𝑢𝑙𝑡 → 𝑛𝑢𝑚𝑠[𝑖 − 1] ∗ 𝑛𝑢𝑚𝑠[𝑘] ∗ 𝑛𝑢𝑚𝑠[𝑗 + 1]
𝑡𝑜𝑝𝐷𝑜𝑤𝑛(𝑛𝑢𝑚𝑠, 𝑘 + 1, 𝑗, 𝑚𝑒𝑚𝑜)
For every interval, if we decide to burst the balloon in the interval last, we get the best from the left and
right sides. Then we add the product of the current balloon with two neighbouring balloons outside
the interval. The reason for that is the fact that when we burst the current balloon last, we do not have
other unburst balloons in the interval. Remember that each time we burst a balloon, two neighbouring
balloons become adjacent.
if (i > j) {
return 0; // base case
}
if (i == j) {
return nums[i]; // base case
}
Step 5. Memoization
We use transforming state variables for memorization.
66
Starting index 𝑖 and ending index 𝑗 are state variables transforming each state. We can use them for
memorization.
if (memo[i][j] != -1) {
return memo[i][j];
}
return result
Solution
if (i == j) {
return (i-1 >= 0 ? nums[i-1] : 1) * nums[i] * (j+1 < nums.size() ? nums[j+1] :
1);
}
if (memo[i][j] != -1) {
return memo[i][j];
}
memo[i][j] = result
return result;
}
67
Analysis
Time-Complexity: 𝑂(𝑛0 )
We are calling the function 𝑛 times and have a total of 𝑛1 states, where 𝑛 is the number of balloons.
Space-Complexity: 𝑂(𝑛1 )
Bottom-Up
We can calculate a base case for an interval with a length less than or equal to three.
int dp[n+1][n+1];
We need to find an answer for the whole interval; therefore, we will be increasing the interval by
increasing the length of the interval (considering one more element each time).
68
}
For every interval, we have several options to burst balloons. We can try to burst balloons at every
index to get the maximum number of coins. Therefore, we need to iterate through indices of balloons
𝑘.
For every interval, we iterate over balloons in the interval. Then we check the maximum number of
coins we can get if we burst the current balloon last. If we burst the current balloon last, we take
maximum values from the left and right sides and add a product of the current balloon with left and
right balloons outside the interval.
69
Step 5. Return Result
After reaching a target state, return an answer for the target state.
return dp[0][n-1]
Solution
int n = (int)nums.size();
int dp[n+1][n+1];
return dp[0][n-1];
}
70
Analysis
Time-Complexity: 𝑂(𝑛0 )
Inside the nested loop, we are performing constant operations 𝑛0 times, where 𝑛 is the number of
balloons.
Space-Complexity: 𝑂(𝑛1 )
71
Longest Common Subsequence
Given two strings, 𝑠𝑡𝑟𝑖𝑛𝑔 1 and 𝑠𝑡𝑟𝑖𝑛𝑔 2, find the length of the longest common subsequence in
these strings.
Top-Down
𝐹(𝑖 + 1, 𝑗)
𝐹(𝑖, 𝑗) → 𝐹(𝑖 + 1, 𝑗 + 1)
𝐹(𝑖, 𝑗 + 1)
For each state, we choose one of the three options and call the recursive function again with updated
indices:
• If characters at indices 𝑖 and 𝑗 match, move indices for both strings 𝑖 + 1 and 𝑗 + 1.
• Move the index 𝑖 + 1 for 𝑠𝑡𝑟𝑖𝑛𝑔 1.
• Move the index 𝑗 + 1 for 𝑠𝑡𝑟𝑖𝑛𝑔 2.
72
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
𝐹(𝑖 + 1, 𝑗 + 1) + 1
𝐹(𝑖, 𝑗) = 𝑚𝑎𝑥 < 𝐹(𝑖 + 1, 𝑗) ?
𝐹(𝑖, 𝑗 + 1)
We get the maximum among all recursive calls and add one if characters at indices 𝑖 and 𝑗 match,
meaning that we are increasing the longest common subsequence by one.
When the index for at least one of the strings reaches a string length, the function reaches the base
case.
if (i == text1.length() || j == text2.length()) {
return 0; // base case
}
Step 5. Memoization
We use transforming state variables for memorization.
The index 𝑖 for 𝑠𝑡𝑟𝑖𝑛𝑔 1 and the index 𝑗 for 𝑠𝑡𝑟𝑖𝑛𝑔 2 are state variables transforming each state. We
can use them for memoization.
if (memo[i][j] != -1) {
return memo[i][j];
}
73
return result
Solution
if (memo[i][j] != -1) {
return memo[i][j];
}
if (text1[i] == text2[j]) {
result = max(result, topDown(text1, text2, i+1, j+1, memo) + 1);
}
memo[i][j] = result;
return result;
}
Analysis
Time-Complexity: 𝑂(𝑛𝑚)
We are calling the function three times and have a total of 𝑛𝑚 states, where 𝑛 is the length of 𝑠𝑡𝑟𝑖𝑛𝑔 1
and 𝑚 is the length of 𝑠𝑡𝑟𝑖𝑛𝑔 2.
Space-Complexity: 𝑂(𝑛𝑚)
Bottom-Up
74
For the base case, assume that both strings are empty. In that case, the longest common subsequence
we can get is 0.
int dp[n+1][m+1];
memset(dp, 0, sizeof(dp));
We need to iterate over both of the strings to compare characters at indices 𝑖 and 𝑗.
For each state, there are three options to come to the current state:
• If characters at these indices 𝑖 and 𝑗 match, we get the possible best result if we ignore these
characters (𝑖 − 1, 𝑗 − 1).
• If characters at these indices don’t match, we get the possible best result if we ignore the
current character of 𝑠𝑡𝑟𝑖𝑛𝑔 1 (𝑖 − 1, 𝑗).
• If characters at these indices don’t match, we get the possible best result if we will ignore the
current character of 𝑠𝑡𝑟𝑖𝑛𝑔 2 (𝑖, 𝑗 − 1).
𝑑𝑝[𝑖 − 1][𝑗 − 1]
𝑑𝑝[𝑖][𝑗] → 𝑑𝑝[𝑖 − 1][𝑗]
𝑑𝑝[𝑖][𝑗 − 1]
75
If characters at indices 𝑖 and 𝑗 match, we add one for the current state, and we find the maximum
among all options.
return dp[n][m]
Solution
int dp[n+1][m+1];
memset(dp, 0, sizeof(dp));
return dp[n][m];
}
76
Analysis
Time-Complexity: 𝑂(𝑛𝑚)
Inside the nested loop, we are performing constant 𝑂(1) operations 𝑛𝑚 times, where 𝑛 is the length of
𝑠𝑡𝑟𝑖𝑛𝑔 1 and 𝑚 is the length of 𝑠𝑡𝑟𝑖𝑛𝑔 2.
Space-Complexity: 𝑂(𝑛𝑚)
77
Best Time to Buy and Sell Stock
Given an array of stock prices on a given day, find the maximum profit you can make by buying and
selling stocks. You are allowed to participate in one transaction at a time (you can sell only if you buy
before).
Intuition
For every stock in the array, we need to make a decision that will lead to the maximum profit. We
need to decide to sell, buy or ignore a stock on the current day.
Top-Down
For each day (state), there are three options to choose from:
• Sell a stock if we have one.
• Ignore the current stock.
• Buy the current stock.
𝑠𝑒𝑙𝑙
𝑟𝑒𝑠𝑢𝑙𝑡 → 𝑠𝑘𝑖𝑝
𝑏𝑢𝑦
78
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
Step 5. Memoization
We use transforming state variables for memorization.
The 𝑖𝑛𝑑𝑒𝑥 of the day and the possibility to sell stock 𝑐𝑎𝑛𝑆𝑒𝑙𝑙 are state variables transforming each
state. We can use them for memoization.
if (memo[index][canSell] != -1) {
return memo[index][canSell];
}
79
Step 6. Return Result
Return a result of a recursive function.
return result
Solution
if (memo[index][canSell] != -1) {
return memo[index][canSell];
}
if (canSell) {
// can sell a product
sell = maxProfit(prices, index+1, false, memo) + prices[index];
} else {
// can buy a product
buy = maxProfit(prices, index+1, true, memo) - prices[index];
}
return result;
}
Analysis
Time-Complexity: 𝑂(𝑛)
We call the function three times and have a total of 𝑛 states, where 𝑛 is the number of days.
Space-Complexity: 𝑂(𝑛)
80
Bottom-Up
The base cases are two options for the first day:
• Buy on the first day and pay 𝑝𝑟𝑖𝑐𝑒𝑠[0].
• Sell and earn on the first day. But on the first day, there isn’t any stock to sell, so the profit is 0.
For every state, there are three options to come to the current state:
• If you decide to buy new stock, consider the case when you previously sold a stock and update
it with a new price.
• If you decide to sell a stock, consider the case when you previously bought the stock and
update it with a new price.
• Ignore the current state (use the previous state when you bought or sold a stock).
81
dp[i][0] = dp[i-1][0]
dp[i][1] = dp[i-1][1]
return dp[n-1][1]
Solution
int n = (int)prices.size();
dp[0][0] = -prices[0];
dp[0][1] = 0;
return dp[n-1][1];
}
82
Analysis
Time-Complexity: 𝑂(𝑛)
Inside the single loop, we are performing constant 𝑂(1) operations 𝑛 times, where 𝑛 is the total
number of days.
Space-Complexity: 𝑂(𝑛)
83
Nim Game
Given a pile of stones and two players, each player can remove 1 to 5 stones in one move. The
player who will remove the last stone will win. Find out whether player 1 can win the game.
Intuition
We simulate the game and find out the case when player 2 loses the game.
Top-Down
There are options to remove 1 to 𝑘 stones for every state, where 𝑘 is the maximum number of stones,
5. We iterate over these options to find out the case that leads to the win.
We try to remove 1 to 𝑘 stones for every state and call a recursive function with the new updated
target.
84
Step 3. Choose Optimal Solution
After coming from recursive calls, we choose an optimal solution among these calls and add the
value for the current state.
After coming from recursive calls, check if one of the options leads to losing the game. If the option
leads to losing the game, we will leave this option for player 2. Consequently, player 1 will win if we will
choose this option.
if (n <= k) {
return true;
}
Step 5. Memoization
We use transforming state variables for memorization.
if (memo[n] != -1) {
return memo[n] == 1;
}
85
Solution
if (memo[n] != -1) {
return memo[n] == 1;
}
memo[n] = 0;
return false;
}
Analysis
Time-Complexity: 𝑂(𝑛𝑘)
We call the function 𝑘 times and have a total of 𝑛 states, where 𝑛 is the total number of stones and 𝑘 is
the number of stones that can be removed.
Space-Complexity: 𝑂(𝑛)
Bottom-Up
Find the base cases for cases we can predict the result.
86
dp[1] = true;
dp[2] = true;
dp[3] = true;
...
dp[k] = true;
For every state, we try to remove 1 to 𝑘 stones and consider the case with removed stones.
If the state with removed stones leads to losing the game, then player 1 wins the game. Otherwise,
player 1 loses the game.
87
}
}
}
return dp[n]
Solution
return dp[n];
}
88
Analysis
Time-Complexity: 𝑂(𝑛𝑘)
Inside the nested loop, we are performing constant 𝑂(1) operations 𝑛𝑘 times, where 𝑛 is the total
number of stones and 𝑘 is the number of stones that can be removed.
Space-Complexity: 𝑂(𝑛)
89