Beginners Guide to Dynamic Programming | Towards Data Science

Dynamic programming is an art, the more problems you solve easier it gets.
Sometimes when you write code it might take some time to execute or it may never run even if your logic is fine. The same problem occurred to me while solving Google Foobar challenge questions and I realized that the solution was not optimized and was using all available RAM (for large values).
An entirely different approach is required to solve such kinds of problems i.e. “optimization of code” by following the concept of dynamic programming.
Dynamic programming is a terrific approach that can be applied to a class of problems for obtaining an efficient and optimal solution.
In simple words, the concept behind dynamic programming is to break the problems into sub-problems and save the result for the future so that we will not have to compute that same problem again. Further optimization of sub-problems which optimizes the overall solution is known as optimal substructure property.
Two ways in which dynamic programming can be applied:
In this method, the problem is broken down and if the problem is solved already then saved value is returned, otherwise, the value of the function is memoized i.e. it will be calculated for the first time; for every other time, the stored value will be called back. Memoization is a great way for computationally expensive programs. Don’t confuse memoization with memorize.
Memoize != memorize
This is an effective way of avoiding recursion by decreasing the time complexity that recursion builds up (i.e. memory cost because of recalculation of the same values). Here, the solutions to small problems are calculated which builds up the solution to the overall problem. (You will have more clarity on this with the examples explained later in the article).
As mentioned above, if you notice that the problem can be broken down into sub-problems and these can be broken into much smaller ones and some of these have overlap (i.e. requires the computation of previously calculated values). The main goal is to optimize the code by reducing the repetition of values by storing the results of sub-problems.
Dynamic Programming can be applied to any such problem that requires the re-calculation of certain values to reach the final solution.
Remember, dynamic programming should not be confused with recursion.
Recursion is a way of finding the solution by expressing the value of a function in terms of other values of that function directly or indirectly and such function is called a recursive function. It follows a top-down approach.
Dynamic programming is nothing but recursion with memoization i.e. calculating and storing values that can be later accessed to solve subproblems that occur again, hence making your code faster and reducing the time complexity (computing CPU cycles are reduced).
Here, the basic idea is to save time by efficient use of space. Recursion takes time but no space while dynamic programming uses space to store solutions to subproblems for future reference thus saving time.
Let’s start with a basic example of the Fibonacci series.
Fibonacci series is a sequence of numbers in such a way that each number is the sum of the two preceding ones, starting from 0 and 1.
F(n) = F(n-1) + F(n-2)
Recursive method:
def r_fibo(n):   if n <= 1:       return n   else:       return(r_fibo(n-1) + r_fibo(n-2))
Here, the program will call itself, again and again, to calculate further values. The calculation of the time complexity of the recursion based approach is around O(2​^N). The space complexity of this approach is O(N) as recursion can go max to N.
For example-
F(4) = F(3) + F(2) = ((F(2) + F(1)) + F(2) = ((F(1) + F(0)) + F(1)) + (F(1) + F(0))
In this method values like F(2) are computed twice and calls for F(1) and F(0) are made multiple times. Imagine the number of repetitions if you have to calculate it F(100). This method is ineffective for large values.
Top-Down Method
def fibo(n, memo):  if memo[n] != null:    return memo[n]  if n <= 1:    return n  else:    res = fibo(n-1) + fibo(n+1)    memo[n] = res    return res
Here, the computation time is reduced significantly as the outputs produced after each recursion are stored in a list which can be reused later. This method is much more efficient than the previous one.
Bottom down
def fib(n):  if n<=1:    return n  list_ = [0]*(n+1)  list_[0] = 0  list_[1] = 1   for i in range(2, n+1):    list_[i] = list_[i-1] + list[i-2]  return list_[n]
This code doesn’t use recursion at all. Here, we create an empty list of length (n+1) and set the base case of F(0) and F(1) at index positions 0 and 1. This list is created to store the corresponding calculated values using a for loop for index values 2 up to n.
Unlike in the recursive method, the time complexity of this code is linear and takes much less time to compute the solution, as the loop runs from 2 to n, i.e., it runs in O(n). This approach is the most efficient way to write a program.
Time complexity: O(n) <<< O(2​^N)
Now, let’s see another example (this is an intermediate level problem):
Problem statement: You have to build a staircase in such a way that, each type of staircase should consist of 2 or more steps. No two steps are allowed to be at the same height — each step must be lower than the previous one. All steps must contain at least one brick. A step’s height is classified as the total amount of bricks that make up that step.For example, when N = 3, you have only 1 choice of how to build the staircase, with the first step having a height of 2, and the second step having a height of 1 i.e.(2,1). But when N = 5, there are two ways you can build a staircase from the given bricks. The two staircases can have heights (4, 1) or (3, 2).
Write a function called solution(n) that takes a positive integer n and returns the number of different staircases that can be built from exactly n bricks. n will always be at least 3 (so you can have a staircase at all), but no more than 200.
This is a problem I had to solve at level 3 of Google Foobar Challenge. I would suggest you try this question on your own before reading the solution, it will help you understand the concept better.
An intuitive approach to this problem:
My first intuitive approach was to create a list l of integers till n.
Then append all the possible combinations of integers of list l into a new list sol.
And, at the final step, I used a for loop to check the sum of every element of the list solthat if it is equal to the required value. If the condition is true that element is appended to another new list final. And the length offinal is returned as a final solution to the problem.
from itertools import combinationsdef solution(n):  l = []  for i in range(1,n):    l.append(i)  sol = []  for k in range(2,len(l)+1):    for m in combinations(l,k):      sol.append(m)  final = []  for z in (sol):    if sum(z) == n :      final.append(z)  steps = len(final)  return (steps)solution(100)
This code turned out to be very ineffective and didn’t work for large values because of the same reason i.e. hight time complexity and repeated calculations of certain values. Running this code for large values(like 100) will use all available RAM and code will eventually crash.
Bottom-up approach for the same problem:
def solution(n):  a = [1]+[0]* n  for i in range(1, n+1):     for k in reversed(range(i, n+1)):      a[k] = a[k-i] + a[k]  return a[n] - 1
At the first step, an empty list ‘a’ is initiated to store all the values from the further loops.
After each iteration of the outer loop, a[j] is the number of staircases you can make with height at most i where j is the number of bricks used.
List a is initiated to [1,0,0,...] because there can be only one stair with 0 blocks and 0 height.
In each iteration of the inner loop, list a is transformed from representing max-height i-1 to representing max-height i, by incorporating the possibility of adding a step of height i to any shorter staircase that leaves you with at least i blocks.
In the final step, the number of different staircases that can be built from exactly nbricks is returned by the function (1 is subtracted at the end to exclude the case of single stair of height n).
This method is effective for large values as well since the time complexity is traded for space here.
This kind of approach can be applied to other problems as well, you just need to identify them and apply the basics of dynamic programming and you will be able to solve the problems efficiently.
Dynamic programming is a very effective technique for the optimization of code. This technique is really simple and easy to learn however it requires some practice to master.
“Those who cannot remember the past are condemned to repeat it.”
-George Santayana
Check this out: https://www.fiverr.com/share/LDDp34