






Study with the several resources on Docsity
Earn points by helping other students or get them with a premium plan
Prepare for your exams
Study with the several resources on Docsity
Earn points to download
Earn points by helping other students or get them with a premium plan
This homework assignment explores fundamental dynamic programming concepts through three challenging problems: the egg drop problem, the longest common subsequence problem, and the maximum subarray sum problem. It provides a comprehensive understanding of dynamic programming techniques, including subproblem definition, recurrence relations, base cases, runtime analysis, and space optimization. The assignment includes detailed solutions and explanations for each problem, making it a valuable resource for students studying algorithms and data structures.
Typology: Exercises
1 / 12
This page cannot be seen from the preview
Don't miss anything!







Due Saturday 3/15/2025, at 10:00 pm (grace period until 11:59pm)
List the names and SIDs of the members in your study group. If you have no collaborators, explicitly write “none”.
You are given m identical eggs and an n story building. You need to figure out the highest floor b ∈ { 0 , 1 , 2 ,... n} that you can drop an egg from without breaking it. Each egg will never break when dropped from floor b or lower, and always breaks if dropped from floor b + 1 or higher. (b = 0 means the egg always breaks). Once an egg breaks, you cannot use it any more. However, if an egg does not break, you can reuse it.
Let f (n, m) be the minimum number of egg drops that are needed to find b (regardless of the value of b).
(a) Find f (1, m), f (0, m), f (n, 1), and f (n, 0). Briefly explain your answers. Hint: use ∞ to denote that it is impossible to find b. (b) Consider dropping an egg at floor h when there are n floors and m eggs left. Then, it either breaks, or doesn’t break. In either scenario, determine the minimum remaining number of egg drops that are needed to find b in terms of f (·, ·), n, m, and/or h. (c) Find a recurrence relation for f (n, m). Hint: whenever you drop an egg, call whichever of the egg breaking/not breaking leads to more drops the “worst-case event”. Since we need to find b regardless of its value, you should assume the worst-case event always happens. (d) Write pseudocode to compute f (n, m). In particular, make sure that your code pro- cesses the subproblems in the correct order. (e) Analyze the runtime complexity of your DP algorithm. (f) Analyze the space complexity of your DP algorithm. (g) Is it possible to modify your algorithm above to use less space? If so, describe your modification and re-analyze the space complexity. If not, briefly justify.
Solution:
(a) We have that:
(b) If the egg breaks, we only need to consider floors 1 to h − 1, and we have m − 1 eggs left since an egg broke, in which case we need f (h − 1 , m − 1) more drops. If the egg doesn’t break, we only need to consider floors h + 1 to n, and there are m eggs left, so we need f (n − h, m) more drops. (c) The recurrence relation is
f (n, m) = 1 + min h∈{ 1 ...n}
max{f (h − 1 , m − 1), f (n − h, m)}.
When we drop an egg at floor h, in the worst case, we need max{f (h − 1 , m − 1), f (n − h, m)} drops. Then, the optimal strategy will choose the best of the n floors, so we need minh∈{ 1 ...n} max{f (h − 1 , m − 1), f (n − h, m)} more drops.
(d) We solve the subproblems in increasing order of m, n, i.e.:
for j in range(m+1): for i in range(n+1): solve f(i, j)
(e) We solve nm subproblems, each subproblem taking O(n) time. Thus, the overall run- time is O(n^2 m). (f) The only thing we have to store is the DP array f , which contains nm elements. Thus, the overall space complexity is O(nm).
(g) Yes, it is possible! Notice that in our recurrence relation in part (c), we only need the values of f (·, m) and f (·, m − 1). So we can just store the last two “columns” computed so far. The pseudocode for this would look approximately as follows: def eggdrop(n, m): if m == 0: # base case for m= return float("inf") if n == 0: return 0
curr = [i for i in range(n+1)] # base case for m=
for j in range(2, m+1): prev = copy(curr)
for i in range(j+1): curr[i] = i for i in range(j+1, n+1): curr[i] = 1 + min([ max(prev[h-1], curr[i-h]) for h in range(1, i+1)
In lecture, we covered the longest increasing subsequence problem (LIS). Now, let us consider the longest common subsequence problem (LCS), which is a bit more involved. Given two arrays A and B of integers, you want to determine the length of their longest common subsequence. If they do not share any common elements, return 0.
For example, given A = [1, 2 , 3 , 4 , 5] and B = [1, 3 , 5 , 7], their longest common subsequence is [1, 3 , 5] with length 3.
We will design an algorithm that solves this problem in O(nm) time, where n is the length of A and m is the length of B.
(a) Define your subproblem in words. Hint: looking at the subproblem for Edit Distance may be helpful. (b) Write your recurrence relation and describe your base cases. (A fully correct recurrence relation will always have the base cases specified.) (c) In what order do we solve the subproblems? (d) What is the runtime of this dynamic programming algorithm? (e) Analyze the space complexity of your DP algorithm. Show how to reduce the space complexity of your algorithm to O(min{m, n}) additional memory (i.e. not including the input). Remark: for all space complexity analyses in this class, we only consider writeable auxiliary memory, which does not include any input if kept read-only.
Solution:
Algorithm Description: Let L[i][j] be the length of the LCS between the first i characters of A and j characters of B. (i.e between A[0 : i] and B[0 : j]). Then the final answer will be L[n][m]. The recurrence is given as follows:
L[i][j] =
0 if i = 0 or j = 0 L[i − 1][j − 1] + 1 if A[i] = B[j] max{L[i − 1][j], L[j − 1][i]} otherwise
Correctness: The correctness follows by induction. Observe that if the last characters of A and B match, then any longest common subsequence should have the same last character. Otherwise, at least one of A[i − 1] and B[j − 1] will not be in the solution, so we take the maximum over the possibilities.
Runtime: Our DP has (n + 1)(m + 1) states and calculating each state takes O(1). Hence, the algorithm runs in O(nm).
Space: Naively, we can just store all the states, which would take up O(nm) space. However, we can note that to compute any L[i][j], we only need the subproblems involving i − 1, j − 1,
and i, j. Thus, we simply need to store the last 2 “rows” or “columns” of the DP table, yielding a space complexity of O(2 min{n, m}) = O(min{n, m}).
Here is pseudocode illustrating this algorithm:
def max_subarray_sum(A): n = len(A) dp = [0 for i in range(n)]
dp[0] = A[0]
for i in range(1, n): dp[i] = max(0, dp[i-1]) + A[i]
return max(max(dp), 0)
Note that this solution uses O(n) space, as we need to store the entire dp array. However, when computing a given dp[i], we only use the previous dp[i − 1], so we can optimize the space complexity of this solution by just storing a “running sum”, as follows:
def max_subarray_sum(A): n = len(A)
max_sub_sum = running_sum = A[0]
for i in range(1, n): running_sum = max(0, running_sum) + A[i] max_sub_sum = max(max_sub_sum, running_sum)
return max(max_sub_sum, 0)
This solution uses O(1) space since it only has to keep track of three integers n, max sub sum, and running sum! (Note that we do not count the space complexity of input A when determining this)
Also, if you’ve done a lot of leetcode before, you may recognize this algorithm as Kadane’s algorithm.
Proof of Correctness: We use induction to show that dp[i] is correct for all i ≥ 0.
dp[k + 1] =
dp[k] + A[k] dp[k] ≥ 0 A[k] dp[k] < 0 = max(dp[k], 0) + A[k]
which is exactly our recurrence relation. Runtime Analysis: Since we perform O(n) iterations of constant-time operations, the overall runtime is O(n) Space Analysis: The unoptimized DP algorithm uses O(n) space, as we’re storing the entire dp array. The optimized DP algorithm (i.e. Kadane’s algorithm) uses O(1) space since it only has to keep track of three integers.
(b) Subproblem Definition: dpo[i] denotes the maximum sum subarray with an odd number of odd numbers ending at index i, and dpe[i] denotes the maximum sum sub- array that ends at index i with an even number of odd numbers. Base Cases: dpo[0] = −∞, dpe[0] = A[0]. Recurrence Relation: we compute the dpo and dpe values in increasing order of i (for i = 1... n − 1) with the recurrence:
dpo[i] =
dpo[i − 1] + A[i] if A[i] is even max(A[i], dpe[i − 1] + A[i]) if A[i] is odd
dpe[i] =
max(dpe[i − 1] + A[i], A[i]) if A[i] is even dpo[i − 1] + A[i] if A[i] is odd
Final Answer: for the final answer, we output the largest dpo[i] over all i.
(c) Describe the order in which we should solve the subproblems in your DP algorithm. (d) What is the runtime complexity of your DP algorithm? Provide a justification. (e) What is the space complexity of your algorithm? Provide a justification.
Solution:
(a) Subproblem Definition: We define B[i 1 , j 1 , i 2 , j 2 ] to be the minimum number of cuts needed to separate the sub-matrix A[i 1 ≤ i 2 , j 1 ≤ j 2 ] into pieces consisting either entirely of bitten pieces or clean pieces. (b) Recurrence Relation:
B[i 1 , j 1 , i 2 , j 2 ] = min
0 , if all entries of A[i 1... i 2 , j 1... j 2 ] are equal 1 + B[i 1 , j 1 , i 1 + k, j 2 ] + B[i 1 + k + 1, j 1 , i 2 , j 2 ] for any k ∈ { 1 ,... , i 2 − i 1 } 1 + B[i 1 , j 1 , i 2 , j 1 + k] + B[i 1 , j 1 + k + 1, i 2 , j 2 ] for any k ∈ { 1 ,... , j 2 − j 1 }
Alternatively, you could have also encapsulated the 0 base case in all single-square pieces, and determined if a piece was pure via the merging, see below. (c) Subproblem Order: we solve them in increasing order of (i 2 − i 1 + 1)(j 2 − j 1 + 1). In other words, we solve all the smallest subproblems first (e.g. containing one square) and build our DP array up to our result B[1, ℓ, 1 , w], which covers the entire paper. (d) Runtime Analysis: Two answers are acceptable: O((ℓ + w)ℓ^2 w^2 ) and O(ℓ^3 w^3 ) We have O(ℓ^2 w^2 ) total subproblems: O(ℓw) possibilities for (i 1 , j 1 ), and O(ℓw) pos- sibilities for (i 2 , j 2 ). For each subproblem, we examine up to m possible choices for horizontal splits, and n possible choices for vertical splits. A single split consideration will result in two smaller subproblems, which we can assume have already been solved, so we just need to find the best split, which takes O(ℓ + w) time. In addition, for a subproblem, we also want to check the base case for if the piece is “pure” (contains only clean paper, or contains only bitten paper). Brute force checking this takes O(ℓw) time, for a total subproblem time of O(ℓw + (ℓ + w)) = O(ℓw). Thus, the overall (accepted) runtime is O(ℓ^2 w^2 ) · O(ℓw) = O(ℓ^3 w^3 ). However, this O(ℓw) factor per subproblem can be reduced to O(ℓ + w) (this is not required to receive full points). We can precompute the purities of every single possible subrectangle and store it in a table. Brute-force performs the pre-computation in O(ℓ^3 w^3 ) time, but using prefix sums allows us to do this in just O(ℓw) time. So to solve our recurrence relation, if we can determine purity/impurity in O(1) time (after doing some pre-computation), then we can reach an overall time of O((ℓ + w)ℓ^2 w^2 ). Alternatively, we can initialize all min-cut values of single square pieces to be 0. Then, if it is possible to have some cut such that both resulting pieces have min-cut values of 0, and both resulting pieces are of the same type (clean-only or bitten-only, and we can take any sample of either and compare them), then we ourself are a pure piece. This
would allow you to avoid the entire pre-computation business as mentioned before, and still achieve a runtime of O((ℓ + w)ℓ^2 w^2 ).
(e) Space Complexity Analysis: we have to store the entire DP array for our recurrence relation to work, so the space complexity is O(ℓ^2 w^2 ).