DP - 2
DP - 2
Let us now move to some advanced-level DP questions, which deal with 2D arrays.
For example, T
he given input is as follows-
3 4
3 4 1 2
2 1 8 9
4 7 8 1
The path that should be followed is 3
-> 1 -> 8 -> 1. Hence the output is 13.
Approach:
● Thinking about the r ecursive approach to reach from the cell ( 0, 0) to ( m-1,
n-1), we need to decide for every cell about the direction to proceed out of
three.
● We will simply call recursion over all the three choices available to us, and
finally, we will be considering the one with minimum cost and add the
current cell’s value to it.
● Let’s now look at the recursive code for this problem:
1
import sys
Let’s dry run the approach to see the code flow. Suppose, m = 4 and n = 5; then the
recursive call flow looks something like below:
2
Here, we can see that there are many repeated/overlapping recursive calls(for
example: ( 1,1) is one of them), leading to exponential time complexity, i.e., O(3n). If
we store the output for each recursive call after their first occurrence, we can easily
avoid the repetition. It means that we can improve this using memoization.
import sys
def minCost(cost,i,j,n,m,dp):
# Special Case
if i == n-1 and j==m-1:
return cost[i][j]
# Base Case
if i>=n or j>=m:
return sys.maxsize
3
else:
ans2 = dp[i+1][j]
cost = [[1,5,11],[8,13,12],[2,3,7],[15,16,18]]
n=4
m=3
dp= [[sys.maxsize for j in range(m+1) for i in range(n+1)]
ans = minCost(cost, 0,0,4,3,dp)
print(ans)
Here, we can observe that as we move from the cell (0,0) to (m-1, n-1), in general,
the i-th row varies from 0 to m-1, and the j-th column runs from 0 to n-1. Hence, the
unique recursive calls will be a maximum of (m-1) * (n-1), which leads to the time
complexity of O
(m*n).
To get rid of the recursion, we will now proceed towards the DP approach.
The DP approach is simple. We just need to create a solution array (lets name that
as ans), where:
ans[i][j] = minimum cost to reach from (i, j) to (m-1, n-1)
Now, initialize the last row and last column of the matrix with the sum of their
values and the value, just after it. This is because, in the last row or column, we can
reach there from their forward cell only (You can manually check it), except the cell
(m-1, n-1), which is the value itself.
ans[m-1][n-1] = cost[m-1][n-1]
ans[m-1][j] = ans[m-1][j+1] + cost[m-1][j] (for 0 < j < n)
ans[i][n-1] = ans[i+1][n-1] + cost[i][m-1] (for 0 < i < m)
4
Next, we will simply fill the rest of our answer matrix by checking out the minimum
among values from where we could reach them. For this, we will use the same
formula as used in the recursive approach:
Finally, we will get our answer at the cell (0, 0), which we will return.
The code looks as follows:
R = 3
C = 3
ans[0][0] = cost[0][0]
return ans[m][n]
Note: T
his is the bottom-up approach to solve the question using DP.
5
Problem Statement: LCS (Longest Common Subsequence)
The longest common subsequence (LCS) is defined as the longest
subsequence that is common to all the given sequences, provided that the
elements of the subsequence are not required to occupy consecutive
positions within the original sequences.
Note: S
ubsequence is a part of the string which can be made by omitting none or
some of the characters from that string while maintaining the order of the
characters.
If s1 and s2 are two given strings then z is the common subsequence of s1 and s2, if
z is a subsequence of both of them.
Example 1:
s1 = "abcdef"
s2 = " xyczef"
Here, the longest common subsequence is "cef"; hence the answer is 3 (the length
of LCS).
Example 2:
s1 = "ahkolp"
s2 = " ehyozp"
Approach: Let’s first think of a brute-force approach using r ecursion. For LCS, we
have to match the starting characters of both strings. If they match, then simply we
can break the problem as shown below:
s1 = "x|yzar"
s2 = " x|qwea"
6
The rest of the LCS will be handled by recursion. But, if the first characters do not
match, then we have to figure out that by traversing which of the following strings,
we will get our answer. This can’t be directly predicted by just looking at them, so
we will be traversing over both of them one-by-one and check for the maximum
value of LCS obtained among them to be considered for our answer.
For example:
Suppose, string s
= "xyz" and string t
= "zxay".
We can see that their first characters do not match so that we can call recursion
over it in either of the following ways:
A=
B=
C=
LCS = max(A, B, C)
Check the code below and follow the comments for a better understanding.
7
def lcs(s, t, m, n):
Here, as for each node, we will be making three recursive calls, so the time
complexity will be exponential and is represented as O
(2m+n), where m and n are
the lengths of both strings. This is because, if we carefully observe the above code,
then we can skip the third recursive call as it will be covered by the two others.
Consider the diagram below, where we are representing the dry run in terms of its
length taken at each recursive call:
8
As we can see there are multiple overlapping recursive calls, the solution can be
optimized using m
emoization followed by DP. So, beginning with the memoization
approach, as we want to match all the subsequences of the given two strings, we
have to figure out the number of unique recursive calls. For string s, we can make
at most l ength(s) recursive calls, and similarly, for string t, we can make at most
length(t) recursive calls, which are also dependent on each other’s solution. Hence,
our result can be directly stored in the form of a 2-dimensional array of size
(length(s)+1) * (length(t) + 1) as for string s, we have 0 to length(s) possible
combinations, and the same goes for string t.
So for every index ‘i’ in string s and ‘j’ in string t , we will choose one of the following
two options:
1. If the character s[i] matches t [j], the length of the common subsequence
would be one plus the length of the common subsequence till the i-1 and j-1
indexes in the two respective strings.
2. If the character s[i] does not match t[j], we will take the longest subsequence
by either skipping i-th or j-th character f rom the respective strings.
Hence, the answer stored in the matrix will be the LCS of both strings when the
length of string s will be ‘i’ and the length of string t will be ‘j’.
9
Hence, we will get the final answer at the position m
atrix[length(s)][length(t)].
Moving to the code:
N = 0
M = 0
if i == N or j == M:
return 0
return memo[i][j]
10
Now, converting this approach into the D
P code:
Time Complexity: We can see that the time complexity of the DP and memoization
approach is reduced to O
(m*n) where m
and n
are the lengths of the given strings.
11
Problem Statement: Knapsack
Given the weights and values of ‘N’ items, we are asked to put these items
in a knapsack, which has a capacity ‘C’. The goal is to get the maximum
value from the items in the knapsack. Each item can only be selected once,
as we don’t have multiple quantities of any item.
For example:
If we consider a particular weight ‘w’ from the array of weights with value ‘v’ and the
total capacity was ‘C’ with initial value ‘Val’, then the remaining capacity of the
knapsack becomes ‘C-w’, and the value becomes ‘Val + v’.
12
Let’s look at the recursive code for the same:
Now, the memoization and DP approach is left for you to solve. For the code, refer
to the solution tab of the same. Also, figure out the time complexity for the same by
running the code over some examples and by dry running it.
Practice problems:
The link provided below contains 26 problems based on Dynamic programming
and numbered as A to Z, A being the easiest, and Z being the toughest.
https://atcoder.jp/contests/dp/tasks
13