Dynamic Programming Tutorial – Level 1 (Easy-Medium)- 2/2
For those of you who read the first part of this tutorial, if you had implemented the “Sum of paths in a triangle problem” according to what I had mentioned, you probably(or maybe definitely) must have encountered a “Time Limit Exceeded” error. Ok.. now please don’t say that I was wrong… I had purposely given that code so that you will actually understand what slow and possibly incorrect Dynamic Programming is. “THAT” was an example of what I had meant by slow algorithm in the case of the Subset-Sum problem. The splitting up of sub-problems was right, the recursion was right but still there was some tiny( or large.. :P) error that made it slow. This was because of the so-called “Overlapping sub-problems” or more informally (.. :P) doing the same thing again and again and again… and again.. 😛 . Let me explain to you what it actually means…
This was the example considered..
5
1 7
6 3 4
Here while finding out path(1) and path(7), we have
path(1)=1+max(path(6)+path(3))
path(7)=7+max(path(3)+path(4))
Here we are finding path(3) twice, which really is not necessary as we can already use the result we found in the first computation in the second computation. So if we can somehow store this result somewhere and access this as and when it is required, then the problem can be more efficiently solved. This technique is what makes dynamic programming so effective in solving problems that could have never ever been imagined being solved. This technique is called “memoization” or memorization. “memoization” literally means memorization, but I have no idea why the creators of the word removed the “r” to make it complex.. :P. Try solving the triangle problem now and see if the solution gets accepted… The following is the code snippet(and PS:- It is correct this time.. :P)
int memo[105][105];
int computedp(int a[105][105],int n,int row,int col)
{
if(row==n) return a[row][col];
//SET ALL ELEMENTS OF MEMO TO -1 INITIALLY TO CHECK IF VISITED OR NOT
if(memo[row][col]!=-1) return memo[row][col];
memo[row][col]=max(a[row][col]+computedp(a,n,row+1,col),a[row][col]+ computedp(a,n,row+1,col+1));
return memo[row][col];
}
Always look for a possibility of having overlapping sub-problems. If this happens, yeah you got it…. Use MEMOIZATION.
Ok as now nearly all the basics have been covered, let me move on to some of the well-known applications of dynamic programming…
1. Longest Increasing Subsequence (LIS)
Never say “Longest Increasing Subsequence”. Always say “LIS”. People start feeling that you are some pro programmer (yeah.. pro- programmer… :P) when you start using short forms. Ok.. let me begin by giving the definition of what a subsequence is.. Okay, leave boring definitions I’ll explain by giving examples..
Consider an Array –{1,2,3,4,5,6,7,8,9}
Some of the subsequences are…
1 3 5 6 7 8 9
1 2 4 5
1 6 9
2 5
3 7
4 8 9
5 7 8
Examples which are not subsequences…
4 1 3
6 2 8
1 5 6 7 8 0
9 8 7 6 5 4
3 2
I hope that the examples have provided the definition of a subsequence. Any sequence of elements from left to right is known as a subsequence.
Consider another example… – {4,1,5,2,7,6}
The subsequences are…
4 1 2
4 7 6
1 5 2
1 2 7 6
Examples which are not subsequences…
1 2 4
1 6 7
Ok now the problem of “Longest Increasing Subsequence” would be to find the length of the longest subsequence such that the elements keep on increasing from left to right.
The answer in the previous example would be :- 3
There are many possibilities such as {1,5,6},{1,2,7},{1,2,6}..etc.
REMEMBER :- The LIS solution only gives the length of the longest subsequence and not the elements that comprise it…
Ok.. first of all this is a dynamic programming problem…
The aim of Dynamic Programming is to solve smaller sub-problems and use them to solve larger problems. That’s exactly what I’ll be doing here. But this time I’ll use a slightly different approach. Previously I had shown you an example of a top-down dp solution with memorization. Now I’ll show you how memorization can be directly avoided(but still indirectly present in the solution) using the
So-called bottom-up dp approach. It’s the same thing as its top-down counterpart but instead of going from a larger problem to smaller problems, we start from smaller problems and move on towards the larger problem. (As an exercise, I encourage you to solve the triangle problem using the bottom-up dp approach).
Okay, so coming to defining what the smaller subproblems are…
Let me be a little conclusive over here…. Defining smaller problems is not something what can be learnt by reading tutorials, its something what develops over time the more you practice. Seeing and solving different problems gives you a hint on how the subproblems should be defined so as to solve the main problem. So now…. What exactly could be the smaller problems for the LIS problem. The LIS problem is to find the Longest Increasing Subsequence for an array, say of length ‘n’. So a possible option would be to find the LIS for smaller arrays like n-1 or n-2 … so on till a single element.
Finding solutions for these and combining them will give you the answer for the entire array.
Let’s see an example :- 5 1 2 4 3, an array of length 5
As LIS always deals in an order, that is from left to right,
LIS(1) = 1 {5 is the only element present. Hence length of longest increasing subsequence is 1}
LIS(2) { Remember the 2 over here is the length of the array from left to right}
LIS(2) = 1 { the possible sequences are either 5 or 1. 5,1 is not possible as it is not increasing}
LIS(3) = 2 {1,2 is increasing}
LIS(4) = 3
LIS(5) = 3
So hence Longest Increasing Subsequence of the above example is 3. Seems pretty easy eh..? Solving subproblems and combining them…? So in this example we used our so-called super human brains to solve it. But to program it on a stupid computer is not going to be that easy. Care should be taken to make it perfect…
int dp[105];
int LIS(int a[],int N)
{
for(int i=1;i<=N;i++)dp[i]=1;
//initialise all LISs to 1.. you figure out why..? 😛
for loop i=1 to N
{
for loop j=1 to i-1
dp[i]=max(dp[i],dp[j]+1);
}
}
return dp[N];
}
The above is the code to find the LIS of a sequence. I am not explaining the code, I’ll just introduce you to the logic applied. We check from left to right starting from array of length 1 to array of length N. …Oh yeah and by the way, the array is 1-indexed (i.e it starts from 1 and ends at N). Each time as we are going from left to right, we add 1 element. We compare this element to each of the previous elements. If it is greater(As it is called the longest INCREASING subsequence… duh.. :P), we add 1 to the LIS found at that intermediate element. This maybe hard to understand, but just read the code a few times and you should be comfortable with it.
I’ll take the following example to show it..
4 1 2 3 – N=4
If LIS(3) is being considered..(this means that we are finding the LIS upto length 3 in the array, that is till the element 2), we check if the element 2 is greater than each of the previous elements. If it is greater, we add 1(because this element is greater, thus it increases the length by 1) to the LIS at that point. Continuing this till N, gives us the LIS for the entire array. This is the final answer… And the reason max() is used, is to find the longest on the many possible cases where the element could be greater than the intermediate elements…
2. Coin Denomination Problem
I’ll just briefly state the problem and the solution. I leave it up to you to write the code….
The CD problem(lol.. CD was a bad short form(and just for the info. I made it up)… :P) requires you to find minimum number of coins to be used so that the total value of these coins is equal to a given sum.
For eq:- consider you have these coins 1,3 ,4 . You got to make 10 with this.. The best way would be to use 3+3+4 = 10. The method to find this would again be to use bottom up dp followed by finding the solution to subproblems(where the subproblem means a sum of 1,a sum of 2,…etc..). Following a similar approach as that for LIS, the solution should be obtained…
So that’s pretty much it for easy dp problems. Again as I said before, just knowing the solutions won’t help. When a random real world problem is given to you, you should be in a situation to figure out that it’s a dp problem and apply the concepts to solve it. I have introduced nearly all the concepts involved to get you started into the vast world of dynamic programming..It’s up to you to use them wisely.. :P(that was LAMEEEEEE…). So, that concludes level 1. Level 2 will be mostly about difficult dp problems where the application of dp concepts are not directly implemented. I am still in the process of learning those, so in the meanwhile, login to your favourite programming website, and start DPing….!!!! TC N HF…!!!