W5. Dynamic Programming Patterns
1. Theory
1.1 Introduction to Dynamic Programming
Dynamic Programming (DP) is a powerful algorithmic technique for solving optimization problems by breaking them down into overlapping subproblems, solving each subproblem once, and storing the results to avoid redundant computation. Unlike divide-and-conquer, where subproblems are independent, dynamic programming exploits the fact that subproblems overlap — the same subproblem may appear multiple times in different branches of the recursion.
The name “dynamic programming” is somewhat misleading — it has nothing to do with writing programs dynamically. The term was coined by Richard Bellman in the 1950s. “Programming” here refers to optimization (as in “linear programming”), and “dynamic” refers to solving problems in stages over time.
1.1.1 When to Use Dynamic Programming
Dynamic programming is applicable when a problem has two key properties:
- Optimal substructure: An optimal solution to the problem contains optimal solutions to subproblems. In other words, you can build an optimal solution from optimal solutions to smaller instances of the same problem.
- Overlapping subproblems: The same subproblems are solved multiple times when using a naive recursive approach. This creates an opportunity for optimization by storing solutions.
Real-world analogy: Imagine planning the shortest route from your home to work, passing through various intermediate locations. If you’ve already computed the shortest path from location A to work, you can reuse that information whenever you consider routes passing through A — you don’t need to recalculate it each time.
1.1.2 Optimization Problems
An optimization problem has the following structure:
- A problem may have many possible solutions (candidates)
- Each solution has an associated value (usually numerical)
- The goal is to find a solution with an optimal value (minimum or maximum)
Note that there may be multiple optimal solutions with the same optimal value. Dynamic programming helps us find at least one such optimal solution efficiently.
Example optimization problems we’ve seen:
- Sorting: Among all permutations of an array, find one that is sorted
- Shortest path: Among all paths between two points, find one with minimum total distance
- Maximum subarray: Among all subarrays, find one with maximum sum
1.2 Steps in Developing a Dynamic Programming Algorithm
There are four essential steps to developing a dynamic programming solution:
Step 1: Characterize the structure of an optimal solution
Analyze how an optimal solution can be constructed from optimal solutions to subproblems. This step is crucial — you must identify the recursive structure of the problem.
Important: For dynamic programming to be efficient, the optimal solution must reuse solutions to overlapping subproblems. If subproblems don’t overlap (as in merge sort), divide-and-conquer is more appropriate than dynamic programming.
Step 2: Recursively define the value of an optimal solution
Express the value of an optimal solution in terms of the values of optimal solutions to smaller subproblems. This typically results in a recurrence relation.
Step 3: Compute the values of optimal solutions
Use either a bottom-up (tabulation) or top-down (memoization) approach to compute the optimal values, ensuring each subproblem is solved only once.
Step 4: Reconstruct an optimal solution
Use the information accumulated while computing optimal values to construct an actual optimal solution (not just its value).
Note: Step 4 can be omitted if only the optimal value is needed, not the solution itself.
1.3 Bottom-Up vs Top-Down Approaches
There are two main ways to implement dynamic programming:
1.3.1 Bottom-Up Approach (Tabulation)
The bottom-up approach builds solutions iteratively from smaller to larger subproblems:
- Initialize a table (array) to store solutions to subproblems
- Fill the table starting from the smallest subproblems, working up to larger ones
- Extract the solution for the original problem from the table
Advantages:
- Usually more efficient (no function call overhead)
- Easier to analyze space complexity
- Can optimize space by discarding unneeded values
Disadvantages:
- Must determine the correct order to fill the table
- May compute solutions to unnecessary subproblems
Example: Computing Fibonacci numbers from
1.3.2 Top-Down Approach (Memoization)
The top-down approach uses recursion with caching:
- A recursive function first checks if the result for given parameters is already known
- If yes, it returns the cached result
- Otherwise, it computes the result recursively and stores it before returning
Advantages:
- More intuitive — follows the natural recursive structure
- Only computes necessary subproblems
- Easier to implement (often just add memoization to naive recursion)
Disadvantages:
- Function call overhead
- Risk of stack overflow for deep recursion
- Harder to optimize space usage
Example: Computing Fibonacci with a dictionary/map to cache previously computed values.
1.4 Example: Fibonacci Numbers
The Fibonacci sequence is a classic example to illustrate the difference between naive recursion and dynamic programming.
Sequence:
Recurrence:
1.4.1 Naive Recursive Solution
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)Analysis:
- Time complexity:
where (golden ratio)- The recurrence
gives exponential growth - Upper bound:
- Exact solution:
- The recurrence
- Space complexity:
due to recursion depth
Why is this slow? The call tree has massive redundancy. For example, computing f(6) requires computing f(4) twice, f(3) three times, f(2) five times, etc. The number of calls grows exponentially.
1.4.2 Bottom-Up Dynamic Programming
def fibonacci(n):
fib = [0] * (n + 1)
fib[0] = 0
fib[1] = 1
for i in range(2, n + 1):
fib[i] = fib[i - 1] + fib[i - 2]
return fib[n]Analysis:
- Time complexity:
— single loop from 2 to - Space complexity:
for the array
Can we improve space? Yes! Since we only need the previous two values, we can use just two variables instead of an array, achieving
def fibonacci(n):
if n == 0: return 0
if n == 1: return 1
prev2, prev1 = 0, 1
for i in range(2, n + 1):
current = prev1 + prev2
prev2, prev1 = prev1, current
return prev11.4.3 Top-Down Dynamic Programming (Memoization)
def fibonacci(n):
memo = {}
return fib_memo(n, memo)
def fib_memo(n, memo):
if n in memo:
return memo[n]
if n == 0:
result = 0
elif n == 1:
result = 1
else:
result = fib_memo(n-1, memo) + fib_memo(n-2, memo)
memo[n] = result
return resultAnalysis:
- Time complexity:
— each value computed exactly once - Space complexity:
for the dictionary and recursion stack
1.5 Maximum Subarray Problem
1.5.1 Problem Definition
Input: A sequence of
Output: Indices
Applications:
- Time series analysis: Finding periods of maximum gain or loss
- Stock trading: Finding the best period to buy and hold (input represents daily price changes)
- Signal processing: Detecting regions of maximum intensity
Example: For the array
- The maximum subarray is
(indices 4-7) with sum
Previous solutions we’ve seen:
- Brute force: Check all
possible subarrays — time - Divide and conquer: Split array in half, check left, right, and crossing —
time
Now we’ll see a dynamic programming solution that achieves
1.5.2 Dynamic Programming Approach
Key insight: For each position
- maxSuffixValue(A, i): The maximum sum of any subarray ending at position
- maxSuffix(A, i): The starting index of that maximum suffix
The crucial observation: the maximum suffix ending at position
- Extends the maximum suffix ending at position
(if that suffix has positive sum), or - Starts fresh at position
(if extending would decrease the sum)
Recurrence for maximum suffix sum:
Why this works: If the best subarray ending at position
Recurrence for suffix starting position:
Finding the overall maximum: Track the maximum over all suffix values:
1.5.3 Kadane’s Algorithm
The bottom-up implementation of this idea is known as Kadane’s Algorithm:
def max_subarray(A, n):
max_suffix_sum = A[1]
max_sum = max_suffix_sum
for i in range(2, n + 1):
max_suffix_sum = max(max_suffix_sum + A[i], A[i])
max_sum = max(max_sum, max_suffix_sum)
return max_sumHow it works:
max_suffix_sumtracks the maximum subarray sum ending at the current positionmax_sumtracks the overall maximum seen so far- At each position, we decide: extend the current subarray or start a new one
Analysis:
- Time complexity:
— single pass through the array - Space complexity:
— only two variables
This is optimal! We cannot do better than
To get the actual subarray indices: Maintain additional variables to track the start and end positions:
def max_subarray_with_indices(A, n):
max_suffix_sum = A[1]
max_sum = max_suffix_sum
start = 1 # start of current suffix
best_start = 1 # start of best subarray
best_end = 1 # end of best subarray
for i in range(2, n + 1):
if max_suffix_sum + A[i] > A[i]:
max_suffix_sum = max_suffix_sum + A[i]
else:
max_suffix_sum = A[i]
start = i # start new suffix here
if max_suffix_sum > max_sum:
max_sum = max_suffix_sum
best_start = start
best_end = i
return (best_start, best_end, max_sum)1.6 Longest Common Subsequence Problem
1.6.1 Subsequences
A sequence
In simpler terms: You can obtain
Examples:
is a subsequence of (select indices 2, 5, 6) is a subsequence of (select indices 2, 3, 5, 7) is not a subsequence of because after selecting C at index 3 and D at index 5, there is no C after position 5
Key difference from substring: A substring must be contiguous, but a subsequence need not be.
1.6.2 Common Subsequences
A sequence
Example: For
is a common subsequence is not a common subsequence (not a subsequence of in the correct order) is a common subsequence
1.6.3 Problem Definition
Longest Common Subsequence (LCS) Problem:
Input: Two sequences
Output: A common subsequence of
Applications:
- Text similarity: Measuring how similar two documents or strings are
- DNA analysis: Comparing and searching amino acid chains
- Version control: Computing differences between file versions (the
diffcommand) - Plagiarism detection: Finding copied passages
Brute force approach:
- Enumerate all
subsequences of - For each, check if it’s a subsequence of
(takes time) - Keep track of the longest one found
Time complexity:
1.6.4 Optimal Substructure
Theorem (Optimal Substructure of LCS):
Let
If
: Then and .Explanation: If the last characters match, they must be in the LCS. Remove them and solve the smaller problem.
If
: Then either: , in which case , or , in which case
Explanation: If the last characters don’t match, at least one is not in the LCS. Try removing each and take the better result.
This gives us a recursive structure perfect for dynamic programming!
1.6.5 Recursive Solution
Define
Base case: Empty sequences have LCS of length 0.
Recursive cases:
- Characters match: Include the matching character and solve the smaller problem
- Characters don’t match: Try excluding each character separately and take the maximum
1.6.6 Bottom-Up Dynamic Programming Solution
We build a 2D table
Additionally, we maintain a table
- “XY” (or “↖”): Characters matched, came from
- “X” (or “←”): Came from
(excluded ) - “Y” (or “↑”): Came from
(excluded )
Algorithm:
def LCS_length(X, m, Y, n):
# Create tables
B = [[None] * n for _ in range(m)] # 1-indexed
C = [[0] * (n + 1) for _ in range(m + 1)] # 0-indexed
# Initialize base cases
for i in range(1, m + 1):
C[i][0] = 0
for j in range(1, n + 1):
C[0][j] = 0
# Fill the table
for i in range(1, m + 1):
for j in range(1, n + 1):
if X[i] == Y[j]:
C[i][j] = C[i - 1][j - 1] + 1
B[i][j] = "XY" # diagonal arrow
elif C[i][j - 1] < C[i - 1][j]:
C[i][j] = C[i - 1][j]
B[i][j] = "Y" # up arrow
else:
C[i][j] = C[i][j - 1]
B[i][j] = "X" # left arrow
return C, BAnalysis:
- Time complexity:
— fill an table - Space complexity:
for the tables
Reconstructing the LCS: Follow the arrows in table
def reconstruct_LCS(B, X, i, j):
if i == 0 or j == 0:
return []
if B[i][j] == "XY":
return reconstruct_LCS(B, X, i-1, j-1) + [X[i]]
elif B[i][j] == "Y":
return reconstruct_LCS(B, X, i-1, j)
else:
return reconstruct_LCS(B, X, i, j-1)1.7 Classic Dynamic Programming Problems
1.7.1 Rod Cutting Problem
Problem: A company produces rods of length
Input: Rod length
Output: A way to cut the rod that maximizes total revenue.
Example: For
| Length (m) | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| Price ($) | 1 | 5 | 8 | 9 | 10 | 17 | 17 |
Some options:
- Don’t cut: revenue = $17
- Cut as 1 + 6: revenue = $1 + $17 = $18 ✓ (optimal)
- Cut as 2 + 2 + 3: revenue = $5 + $5 + $8 = $18 ✓ (also optimal)
Optimal substructure: The maximum revenue
- Cutting off a piece of length
(for some ) and selling it for - Optimally cutting the remaining piece of length
This gives the recurrence:
with base case
Dynamic programming solution (bottom-up):
def rod_cutting(p, n):
r = [0] * (n + 1)
for j in range(1, n + 1):
max_revenue = -float('inf')
for i in range(1, j + 1):
max_revenue = max(max_revenue, p[i] + r[j - i])
r[j] = max_revenue
return r[n]Time complexity:
Space complexity:
1.7.2 0-1 Knapsack Problem
Problem: You have a knapsack with capacity
Input:
- Capacity
- Arrays
(weights) and (values)
Output: A subset
Example: Capacity
| Item | Suitcase | Tablet | Laptop | Water | Tent | Plug | Bed |
|---|---|---|---|---|---|---|---|
| Weight (kg) | 12 | 2 | 6 | 3 | 24 | 1 | 9 |
| Value ($) | 36 | 4 | 24 | 2 | 64 | 1 | 22 |
Why greedy doesn’t work: You might think “take items with highest value-to-weight ratio first,” but this doesn’t always give the optimal solution.
Optimal substructure: Define
Explanation:
- Base case: No items or no capacity means zero value
- Item too heavy: Can’t include it, so the answer is the same as without this item
- Can include item: Choose the better option between including it or not
Dynamic programming solution:
def knapsack(W, w, v, n):
K = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for wt in range(1, W + 1):
if w[i] > wt:
K[i][wt] = K[i-1][wt]
else:
K[i][wt] = max(K[i-1][wt], v[i] + K[i-1][wt - w[i]])
return K[n][W]Time complexity:
Space complexity:
Note on complexity: This is called pseudo-polynomial because the running time depends on the numeric value of
1.8 Comparison: Divide-and-Conquer vs Dynamic Programming
Both paradigms break problems into subproblems, but they differ in a crucial way:
| Aspect | Divide-and-Conquer | Dynamic Programming |
|---|---|---|
| Subproblem overlap | Subproblems are independent | Subproblems overlap |
| When to use | Subproblems solved once each | Same subproblem solved many times |
| Implementation | Direct recursion | Tabulation or memoization |
| Examples | Merge sort, quicksort, binary search | Fibonacci, LCS, knapsack, rod cutting |
| Memory | Recursion stack only | Stores all subproblem solutions |
Key insight: If you find yourself solving the same subproblem repeatedly in your recursion tree, dynamic programming can help by storing those solutions.
2. Definitions
- Dynamic Programming (DP): An algorithmic technique for solving optimization problems by breaking them into overlapping subproblems, solving each once, and storing results to avoid redundant computation.
- Optimization Problem: A problem with many candidate solutions, each with an associated value, where the goal is to find a solution with optimal (minimum or maximum) value.
- Optimal Substructure: A property where an optimal solution to a problem contains optimal solutions to its subproblems.
- Overlapping Subproblems: A property where the same subproblems are encountered multiple times when solving a problem recursively.
- Bottom-Up Approach (Tabulation): A DP implementation that iteratively fills a table from smallest to largest subproblems.
- Top-Down Approach (Memoization): A DP implementation using recursion with a cache to store and reuse previously computed results.
- Memoization: The technique of storing the results of expensive function calls and returning the cached result when the same inputs occur again.
- Subsequence: A sequence derived from another by deleting some elements without changing the order of remaining elements.
- Common Subsequence: A sequence that is a subsequence of two or more given sequences.
- Longest Common Subsequence (LCS): The longest sequence that is a subsequence of two given sequences.
- Maximum Subarray: The contiguous subarray with the largest sum within a one-dimensional array.
- Kadane’s Algorithm: An
algorithm for solving the maximum subarray problem using dynamic programming. - Rod Cutting Problem: An optimization problem of cutting a rod into pieces to maximize revenue given length-dependent prices.
- 0-1 Knapsack Problem: An optimization problem of selecting items to maximize value while staying within a weight capacity constraint, where each item can be taken at most once.
- Pseudo-Polynomial Time: A time complexity that appears polynomial in the numeric value of the input but exponential in the input size (number of bits).
3. Formulas
- Fibonacci Recurrence:
with , - Fibonacci Time Complexity (Naive Recursion):
where - Fibonacci Time Complexity (DP):
- Golden Ratio:
- Maximum Suffix Value:
- Maximum Subarray Time Complexity (Kadane):
- LCS Recurrence:
- LCS Time Complexity:
where and - LCS Space Complexity:
- Rod Cutting Recurrence:
with - Rod Cutting Time Complexity:
- Knapsack Recurrence:
- Knapsack Time Complexity:
(pseudo-polynomial) - Knapsack Space Complexity:
4. Practice
4.1. Developer Assignment to Projects (Problem Set 5, Task 1)
A software development company must deliver
The company has estimated the completion time for each project with different numbers of assigned developers in a table
(1a)(i) How can the optimal solution be expressed in terms of optimal solutions to subproblems that can be computed independently? Show, with a concrete example, that a naïve implementation leads to repeated computation of some subproblems.
(1b) Write pseudocode for a dynamic programming algorithm (top-down or bottom-up) that returns both the minimum total completion time and the specific assignment of developers to projects.
(2) Consider this greedy strategy: process projects in order A, B, C, D, E. For each project, assign the number of developers that minimizes that project’s completion time, then move to the next project with the remaining developers. Show that this does not always yield an optimal assignment by: (i) giving a small counterexample, (ii) showing the greedy assignment and its total time, (iii) showing an optimal assignment and its total time, (iv) briefly explaining why the greedy is suboptimal.
(3) Analyze the asymptotic complexity in the worst case for: (a) the naïve recursive implementation, (b) the dynamic programming algorithm.
(4) Apply the efficient algorithm to the specific case below with
| No. of developers | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| Project A | 24 d | 21 d | 18 d | 14 d | 9 d | 9 d | 8 d | 7 d | |
| Project B | 28 d | 22 d | 20 d | 16 d | 11 d | 10 d | 10 d | 9 d | |
| Project C | 38 d | 31 d | 30 d | 24 d | 20 d | 18 d | 14 d | 13 d | |
| Project D | 34 d | 33 d | 22 d | 20 d | 16 d | 14 d | 12 d | 11 d | |
| Project E | 32 d | 26 d | 21 d | 17 d | 13 d | 11 d | 10 d | 9 d |
Click to see the solution
Key Concept: This is a classic DP problem: distribute
1(a)(i) Optimal substructure:
Define
with base case
The naïve recursive implementation recomputes
1(b) Pseudocode (bottom-up DP):
function ASSIGN-DEVELOPERS(S, D, C):
// dp[k][d] = min total time for first k projects with d developers
dp[0..S][0..D] ← ∞
dp[0][d] ← 0 for all d
assign[k][d] ← 0 // stores how many devs assigned to project k
for k ← 1 to S:
for d ← 0 to D:
for j ← 0 to d:
if C[k][j] + dp[k-1][d-j] < dp[k][d]:
dp[k][d] ← C[k][j] + dp[k-1][d-j]
assign[k][d] ← j
// Reconstruct assignment
remaining ← D
for k ← S downto 1:
devs[k] ← assign[k][remaining]
remaining ← remaining - devs[k]
return dp[S][D], devs
2. Greedy counterexample:
Consider just 2 developers, 2 projects, with times:
- (i) Counterexample: 2 developers, 2 projects;
, , , . - (ii) Greedy: Assign 2 developers to A (time = 1), then 0 remaining for B — impossible. So assign 1 to A (time = 5), then 1 to B (time = 5). Total = 10.
- (iii) Optimal: Assign 1 to A and 1 to B. Total = 5 + 5 = 10. (In this symmetric case greedy is optimal. A better counterexample is asymmetric tables where greedily taking the minimum per-project diverges from the global optimum.)
- (iv) The greedy strategy looks only at the current project’s cost and does not account for how remaining developers affect future projects. A locally optimal allocation of developers to one project can leave too few for later projects, increasing overall cost.
3. Complexity analysis:
- (a) Naïve recursion: The recursion tree has
levels; at each level we branch into up to choices, giving calls in the worst case — exponential in . - (b) DP algorithm: We fill a table of size
(i.e., states), and for each state we iterate over choices. Total time: . Space: .
4. Application to the given table (
We compute
After
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|---|
| 24 | 21 | 18 | 14 | 9 | 9 | 8 | 7 | ||
| assign A | — | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
For subsequent projects, the algorithm iterates over all ways to split
Final answer (optimal for all 8 developers, 5 projects):
To find the minimum, enumerate all valid allocations with
By enumeration of promising assignments:
- A=1(24), B=1(28), C=1(38), D=1(34), E=4(17): total = 24+28+38+34+17 = 141 — too many projects with 1 dev
- A=2(21), B=2(22), C=1(38), D=1(34), E=2(26): sum devs = 8, total = 141
- A=3(18), B=2(22), C=1(38), D=1(34), E=1(32): devs=8, total = 144
- A=2(21), B=1(28), C=2(31), D=1(34), E=2(26): devs=8, total = 140
- A=1(24), B=2(22), C=2(31), D=1(34), E=2(26): devs=8, total = 137
- A=2(21), B=2(22), C=2(31), D=1(34), E=1(32): devs=8, total = 140
- A=1(24), B=2(22), C=1(38), D=2(33), E=2(26): devs=8, total = 143
- A=2(21), B=1(28), C=1(38), D=2(33), E=2(26): devs=8, total = 146
- A=1(24), B=2(22), C=2(31), D=2(33), E=1(32): devs=8, total = 142
- A=2(21), B=2(22), C=2(31), D=2(33), E=0: impossible (Project E needs ≥1)
- A=1(24), B=1(28), C=2(31), D=2(33), E=2(26): devs=8, total = 142
- A=2(21), B=1(28), C=2(31), D=2(33), E=1(32): devs=8, total = 145
- A=1(24), B=2(22), C=3(30), D=1(34), E=1(32): devs=8, total = 142
- A=2(21), B=2(22), C=1(38), D=2(33), E=1(32): devs=8, total = 146
- A=1(24), B=3(20), C=2(31), D=1(34), E=1(32): devs=8, total = 141
- A=2(21), B=3(20), C=1(38), D=1(34), E=1(32): devs=8, total = 145
- A=1(24), B=3(20), C=1(38), D=2(33), E=1(32): devs=8, total = 147
- A=3(18), B=1(28), C=2(31), D=1(34), E=1(32): devs=8, total = 143
- A=1(24), B=2(22), C=3(30), D=2(33), E=0: impossible
- A=2(21), B=2(22), C=3(30), D=1(34), E=0: impossible
- A=1(24), B=1(28), C=3(30), D=2(33), E=1(32): devs=8, total = 147
- A=2(21), B=1(28), C=3(30), D=1(34), E=1(32): devs=8, total = 145
- A=3(18), B=2(22), C=2(31), D=1(34), E=0: impossible
- A=1(24), B=2(22), C=2(31), D=1(34), E=2(26): devs=8, total = 137 ← candidate
The DP finds the globally optimal assignment. The minimum total completion time is 137 days, achieved by:
- Project A: 1 developer → 24 days
- Project B: 2 developers → 22 days
- Project C: 2 developers → 31 days
- Project D: 1 developer → 34 days
- Project E: 2 developers → 26 days
- Total: 1+2+2+1+2 = 8 developers, 24+22+31+34+26 = 137 days
Answer: Minimum total completion time is 137 days with assignment A=1, B=2, C=2, D=1, E=2 developers.
4.2. Compute Fibonacci Number Recursively (Lecture 5, Example 1)
Compute
Click to see the solution
Key Concept: Trace the recursive calls to see the massive redundancy.
Recursive definition:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) for n ≥ 2
Call tree for F(6):
F(6)
/ \
F(5) F(4)
/ \ / \
F(4) F(3) F(3) F(2)
/ \ / \ / \ / \
F(3) F(2) F(2) F(1) F(2) F(1) F(1) F(0)
... (continues with more branches)
Count of calls for each value:
: 5 calls : 8 calls : 5 calls : 3 calls : 2 calls : 1 call : 1 call
Total calls:
More precisely, the number of calls
Answer: Computing
4.3. Fibonacci with Bottom-Up DP (Lecture 5, Example 2)
Compute
Click to see the solution
Key Concept: Build the table iteratively from
Table construction:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 |
Steps:
- Initialize:
, - For
to :
Answer:
4.4. Maximum Subarray Using Kadane’s Algorithm (Lecture 5, Example 3)
Find the maximum subarray of
Click to see the solution
Key Concept: Track the maximum sum ending at each position and the overall maximum.
Trace through the algorithm:
| 1 | -2 | — | — | -2 | -2 | -2 |
| 2 | 1 | -2 | 1 | 1 ✓ | 1 | |
| 3 | -3 | 1 | -3 | -2 | 1 | |
| 4 | 4 | -2 | 4 | 4 ✓ | 4 | |
| 5 | -1 | 4 | -1 | 3 | 4 | |
| 6 | 2 | 3 | 2 | 5 | 5 | |
| 7 | 1 | 5 | 1 | 6 | 6 | |
| 8 | -5 | 6 | -5 | 1 | 6 | |
| 9 | 4 | 1 | 4 | 5 | 6 |
Explanation:
- At position 2, extending the suffix from position 1 gives
, but starting fresh at position 2 gives → choose 1 - At position 4, extending gives
, but starting fresh gives → choose 4 (start new subarray) - From positions 4-7, we keep extending (all extensions are better than starting fresh)
- The maximum overall is 6, occurring at position 7
To find indices: The maximum subarray is
Answer: Maximum subarray sum is 6, from indices 4 to 7.
4.5. LCS of Two Sequences (Lecture 5, Task 1)
Apply the LCS algorithm to:
Click to see the solution
Key Concept: Build the DP table comparing all prefixes of
Build the table
| B | D | C | A | B | A | ||
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| A | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| B | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
| B | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
Reconstructing the LCS: Trace backward from
: , diagonal → Include B : , diagonal → Include A : , came from left or up- Continue tracing…
One LCS is
Answer: The length of the LCS is 4. One LCS is
4.6. Rod Cutting Problem (Lecture 5, Task 2)
Apply the rod cutting algorithm to a rod of length
| Length |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Price |
1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
Fill in the table for optimal revenue
Click to see the solution
Key Concept: For each length, try all possible first cuts and take the maximum.
DP table calculation:
| Length |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Revenue |
0 | 1 | 5 | 8 | 10 | 13 | 17 | 18 | 22 | 25 | 30 |
Step-by-step calculations:
: : : : : : : : (best is ) : (best is ) : (don’t cut at all)
Answer: The optimal revenue for length 10 is $30. One optimal cutting is to not cut at all (sell the whole rod for $30).
4.7. 0-1 Knapsack Problem (Lecture 5, Task 3)
Fill in the DP table for the knapsack problem with capacity
| Item | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| Weight (kg) | 12 | 2 | 6 | 3 | 24 | 1 | 9 |
| Value ($) | 36 | 4 | 24 | 2 | 64 | 1 | 22 |
Click to see the solution
Key Concept: Build a table
Analysis: For capacity 24 kg:
- Item 5 (tent) weighs exactly 24 kg and has value $64
- This single item gives maximum value
- Other combinations give less:
- Items 1+3+6: weight = 12+6+1 = 19 kg, value = 36+24+1 = $61
- Items 1+7+6: weight = 12+9+1 = 22 kg, value = 36+22+1 = $59
Answer: Maximum value is $64, achieved by taking only the tent (item 5).
4.8. Compare Fibonacci Implementations (Lecture 5, Task 4)
Compare the number of operations for computing
- Naive recursion
- Memoization
- Bottom-up DP
Click to see the solution
Key Concept: Count the number of addition operations or function calls.
(a) Naive Recursion:
- Number of calls:
- For
: , so approximately calls - Each non-base call performs 1 addition
- Total additions: Approximately 6,765
(b) Memoization:
- Each value
is computed exactly once - Total function calls:
- Total additions:
(since and don’t require addition) - Total operations: 21 lookups/stores + 19 additions = 40 operations
(c) Bottom-Up DP:
- Loop from
to : that’s 19 iterations - Each iteration: 1 addition
- Total operations: 19 additions
Comparison:
| Method | Function Calls | Additions | Complexity |
|---|---|---|---|
| Naive Recursion | ~13,529 | ~6,765 | |
| Memoization | 21 | 19 | |
| Bottom-Up DP | 0 (no recursion) | 19 |
Answer: Bottom-up DP is the most efficient, requiring only 19 additions. Naive recursion is exponentially slower.
4.9. Rod Cutting with Cost Per Cut (Lecture 5, Task 5)
Modify the rod cutting problem: each cut costs
Click to see the solution
Key Concept: Subtract the cutting cost when making a cut.
Modified recurrence:
If we make a cut, we get:
- Revenue from the first piece:
- Revenue from the remaining piece:
- But: We pay cost
for making the cut
So the recurrence becomes:
Alternatively:
Key difference: Now it might be optimal to make fewer cuts (or no cuts) if the cutting cost is high.
Example: If
- No cut: revenue = $9
- Cut as 1+3: revenue = $1 + $8 - $2 = $7
- Cut as 2+2: revenue = $5 + $5 - $2 = $8
- Cut as 1+1+2: revenue = $1 + $1 + $5 - $4 = $3 (two cuts!)
Best: No cut, revenue $9.
Answer: The recurrence includes