Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Dynamic Programming Series - Introduction
Ankur Sheel
Ankur Sheel

Posted on • Edited on • Originally published atankursheel.com

     

Dynamic Programming Series - Introduction

This article has been cross-posted fromhere

Dynamic Programming is one of those techniques that every programmer should have in their toolbox. But, it is also confusing for a lot of people. For a long time, I struggled to get a grip on how to apply Dynamic Programming to problems. Most articles that I could find on the internet, gave the final dynamic programming solution without actually showing the approach taken to arrive at the final solution.

In this article, I will show the benefits of using a Dynamic Programming approach to solving problems with an example. In the end, I will show some steps you can use to find a Dynamic Programming solution. Hopefully, after reading this article, you will find Dynamic Programming simple and intuitive.


What is Dynamic Programming?

Dynamic programming is an efficient method for solving specific types of complicated computational problems. These problems are generally those that can be broken down into smaller overlapping sub-problems. It can be characterised basically as recursion along with memoization. Memoization is the ability to save the results of specific states to reuse later.


Profiling

To prove that we are improving our solution, we need statistics that we can compare. I will be usinggoogle benchmark to help profile our solutions. The benchmark will look like this

Benchmark NameRunning TimeIterations/secItems/sec
  1. Benchmark Name: The name of the benchmark. It will be the format FunctionName/value passed in.
  2. Running Time: The time it took for our function to return a result.
  3. Iterations/sec: The number of times the function could be invoked in 1 second.
  4. Items/sec:Â The number of items that were processed in 1 second.

While 2 and 3 will give us an indication of the time complexity of the function, 4 will give us the space complexity.


Example: Fibonacci Series

The classic example to explain dynamic programming is theFibonacci computation which can be formalised as follows

Fibonacci(n)=0;ifn=0Fibonacci(n)=1;ifn=1Fibonacci(n)=Fibonacci(n-1)+Fibonacci(n-2);ifn>=2
Enter fullscreen modeExit fullscreen mode

Naive Recursive Approach

The Fibonacci sequence can easily be solved by the following recursive method:

longFibonacci(longn){if(n==0){return0;}if(n==1){return1;}returnFibonacci(n-2)+Fibonacci(n-1);}
Enter fullscreen modeExit fullscreen mode

On running the above code and profiling it on my machine, I get:

Benchmark NameRunning TimeIterations/secItems/sec
FibonacciRecursive/100 ms248888917.0685M
FibonacciRecursive/200 ms21333303.026k
FibonacciRecursive/308 ms1793.60887k
FibonacciRecursive/40997 ms140
FibonacciRecursive/50124510 ms10.40282

Although this method returns almost instantly forn <= 30, it takes a little less than a second forn = 40 and approximately 2 minutes forn = 50. Why is the amount of running time increasing so rapidly? This can be explained easily by following the execution stack. Let's do this forn = 6 to keep it simple. The following image shows the sequence of calls that are made.Fibonacci Series Fibonacci Series : From Wikimedia Commons

Looking at the image, we can see that to calculatefibonacci(6), we calculateÂfibonacci(5) 1 time,Âfibonacci(4) 2 times_, _fibonacci(3) 3 times,Âfibonacci(2) 5 times_, fibonacci(1) 8 times_ and fibonacci(0) 5 times. Throughout the call-stack, we are repeatedly computing values that we have already computed. This amount of duplicated work being done keeps on increasing asn becomes larger. You must have realised that this solution is not at all scalable. If you are thinking that there has to be a better way, you are correct.

Top-Down Recursive approach with Memoization

The 1st step to improving the above solution is to add memoization i.e to store the previously computed values in some kind of a data structure. Although you can use any data structure that you like, for the purposes of this example, I will use a map.

#define MOD 1000000007longFibonacciMemonized(longn){std::map<long,long>computedValues;computedValues.insert(make_pair(0,1));computedValues.insert(make_pair(1,1));returnFibonacciMemonized(n,computedValues);}longFibonacciMemonized(longn,std::map<long,long>&computedValues){if(computedValues.find(n)!=computedValues.end()){returncomputedValues[n];}longnewValue=(FibonacciMemonized(n-1,computedValues)+FibonacciMemonized(n-2,computedValues))%MOD;computedValues.insert(make_pair(n,newValue));returnnewValue;}
Enter fullscreen modeExit fullscreen mode

The top method is our main method. All it does is add the 2 base cases to a map and then calls the bottom method with the map as one of the arguments. This bottom method is our recursive method. In this method, we check if the map contains the computed value. If it does. then we just return that value, otherwise, we compute the value forn-1 andÂn-2. We mod the result usingÂ1000000007 to avoid overflows. Before, returning their sum, we store the value in our map. How better is this version? Let's look at the benchmark results

Benchmark NameRunning TimeIterations/secItems/sec
FibonacciMemonized/10000 ms40733.60284M
FibonacciMemonized/50002 ms8962.90891M
FibonacciMemonized/100003 ms4072.82288M
FibonacciMemonized/150005 ms2422.66937M
FibonacciMemonized/200007 ms1872.65432M

We can see that we have reduced the amount of time drastically. Even forn = 20000, the result is instantaneous. However, there is a problem with this approach. Can you spot it? If you said memory usage, you are absolutely correct.  Although the new version is much faster, it is still a recursive algorithm. And, the problem with recursive algorithms is that each recursive call takes some space on the stack. A high enoughn, and we run the risk of running out of memory. Let's see why this happens with an example whereÂn = 100. Because, we don't have the result when we start, we call the method recursively for 999, 998, 997 ... 1. At that point, we have all the computed results in our map. Now, as we return from our recursive functions, we just lookup the value in the table and return it. So, even though we have reduced the number of recursive calls, we still maken recursive calls before getting our initial result. This can easily be seen by comparing the iteration/seconds between this and the previous algorithm. Let's try something better.

Bottom Up Approach with Dynamic Programming

In the previous approach, our main problem was the recursive nature of our algorithm. Let's see if we can get rid of it by using an iterative approach. How do we do this? Instead of starting from the final value, we will start with the smallest value ofÂn and build up the results.

#define MOD 1000000007longFibonacciDP(longn){if(n==0){return0;}if(n==1){return1;}long*results=newlong[n+1];results[0]=0;results[1]=1;for(inti=2;i<=n;i++){results[i]=(results[i-1]+results[i-2])%MOD;}longvalue=results[n];delete[]results;returnvalue;}
Enter fullscreen modeExit fullscreen mode

In the above function, we have an array ofÂn+1 to store the results. We initialise the array for our base cases ofn=0Â andn=1Â **and then start iterating from **2 ton. At each step, we can use the 2 previously computed values and finally return the result. Lets again look at the benchmark results to see how does this approach do?

Benchmark NameRunning TimeIterations/secItems/sec
FibonacciDP/1000001 ms1906130.711M
FibonacciDP/6000005 ms280111.456M
FibonacciDP/110000010 ms145110.626M
FibonacciDP/160000014 ms100112.249M
FibonacciDP/210000018 ms81110.448M

Even whenÂn=210,000, this approach returns almost instantly. At the same time, since, this algorithm is not recursive in nature, we have drastically reduced the amount of space that is required. We can see this by comparing the items/sec which is decreasing much slower even thoughÂn is increasing more rapidly than before. Now you may be thinking, that since we have got a linear time complexity and linear space complexity, this is the end. In most cases, that would be true. But, in this case, we can optimise our solution further.

Bottom Up Approach with Dynamic Programming (Optimised)

In the last algorithm, the amount of space required is proportional toÂn. This is because we are storing all the results. But, we don't need to store all of them. Let's get rid of the array by using just 3 variables - 2 to store the previous results and 1 to store the current result.

#define MOD 1000000007longFibonacciDPOptimized(longn){if(n==0){return0;}if(n==1){return1;}longn1=0;longn2=1;longcurrent=0;for(inti=2;i<=n;i++){current=(n1+n2)%MOD;n1=n2;n2=current;}returncurrent;}
Enter fullscreen modeExit fullscreen mode

Although this algorithm is doing exactly the same thing as the previous one, we have reduced our space complexity to be constant since the amount of space needed is no longer dependent onÂn. Again the benchmark results for comparison

Benchmark NameRunning TimeIterations/secItems/sec
FibonacciDPOptimized/1000000 ms2987202.569M
FibonacciDPOptimized/6000003 ms498207.242M
FibonacciDPOptimized/11000005 ms280202.138M
FibonacciDPOptimized/16000007 ms187205.188M
FibonacciDPOptimized/210000010 ms128205.0708M

Here, we can see that although **n **is increasing, the items/sec is more or less the same. This is the best we can do and no further optimisations are possible.


Conclusion

From the above example, we can see that we only need to identify overlapping subproblems and then avoid duplicated work by caching the computed results. To recap, we can use these steps to find a dynamic programming approach to our problem

  1. Find the overlapping subproblem.
  2. Start with a recursive solution
  3. Modify the recursive solution to use a top-down memoized version.
  4. Remove the recursion by making it an iterative solution.
  5. If you don't need to keep all the previous results, keep only the ones that are required.

If you would like to run the code yourself, you can find the codehereÂ


Hopefully, this article has removed the mystery around Dynamic Programming. In the next few articles in the series, we will look at some of the more common problems that can be solved by Dynamic Programming.and use the above steps to come up with a solution. Have you tried Dynamic Programming before? How was your experience? Let me know in the comments.

Top comments(11)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
jvanbruegge profile image
Jan van Brügge
Student of the technical university munich, chief software engineer for the MOVE-II CubeSat, likes functional programming and sports
  • Joined

You forgot that C++ has tail call optimization. This means you can run it in constant space:

long Fibbonacci(long n) {    auto helper = [](long a, long b, long n) { return n > 0 ? helper(b, a+b, n-1) : a; }    return helper(0, 1, n);}
Enter fullscreen modeExit fullscreen mode

this is similar to the for loop

CollapseExpand
 
ankursheel profile image
Ankur Sheel
  • Joined

You are right about the tail call optimisation. However, that would have beaten the purpose of gearing this article towards beginners and also keeping it language agnostic. The main issue I have had with other articles (and that I wanted to avoid here) is that either they skip intermediate steps or use tricks which are not conducive to understanding the technique itself.

CollapseExpand
 
mortoray profile image
edA‑qa mort‑ora‑y
I'm a creative programmer and puzzles designer.I cook monsters.
  • Email
  • Education
    Lots
  • Work
    Programmer, Writer, Speaker, Puzzle Designer at Edaqa's Room
  • Joined

You left in the#define MOD 1000000007 and% MOD which isn't part of the explanation. Though I recall see computing tests that have this requirement on them. :)

CollapseExpand
 
ankursheel profile image
Ankur Sheel
  • Joined
• Edited on• Edited

I do mention the reason in the Top-Down Recursive approach with Memoization section. Re-iterating it here for your convenienceWe mod the result using 1000000007 to avoid overflows.

CollapseExpand
 
ollpu profile image
Roope Salmi
  • Joined

Oh, further optimization is possible. For example, with matrix exponents ;)

CollapseExpand
 
ankursheel profile image
Ankur Sheel
  • Joined

Haha. You got me there. I should have specified no further optimizations from a DP perspective :)

CollapseExpand
 
wozaisuzhou profile image
Danny
Ex-Cisco , Ex-Webex , Boeing - Senior Full Stack Developer. Love : E-commerce web 1 - web 3 technologies Current research : Ethereum and blockchain
  • Location
    Vancouver
  • Joined

Another solution , probably can take a look at :

dev.to/wozaisuzhou/algorithms-chal...

CollapseExpand
 
joshcheek profile image
Josh Cheek
  • Joined

Good article! I think C++ isn't the right language for it, though (syntactic noise, difficulty reproducing, the numbers are too large for it to handle).

CollapseExpand
 
ankursheel profile image
Ankur Sheel
  • Joined

Thanks Josh.

I feel that since it is a technique, it is not limited to a language. I guess it might be more accessible if it was in one of the newer languages. But, it's better to stick with a language you are comfortable with and I don't need to figure out a whole new benchmarking system :)
I could have written it in pseudocode but then the benchmarking would have been extremely difficult.

What would you suggest be done differently?

CollapseExpand
 
amlace profile image
Suraj
  • Joined

Wonderful post :-) Cleared up a lot of concepts for me.

CollapseExpand
 
ankursheel profile image
Ankur Sheel
  • Joined

Thanks Suraj.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Joined

More fromAnkur Sheel

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp