Overlapping Subproblems and Optimal Substructure in DSA Rust - Time & Space Complexity
We want to understand how solving problems with repeated parts affects the time it takes to run the code.
How does breaking a problem into smaller parts and reusing answers change the work done?
Analyze the time complexity of the following recursive Fibonacci function.
fn fib(n: u32) -> u32 {
if n <= 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}
This function calculates the nth Fibonacci number by calling itself twice for smaller values.
Look for repeated work inside the function.
- Primary operation: Recursive calls to fib(n - 1) and fib(n - 2)
- How many times: Many calls repeat the same fib values multiple times, especially for larger n.
Each call splits into two more calls, doubling work roughly.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 177 calls |
| 20 | Over 21,000 calls |
| 30 | More than 2 million calls |
Pattern observation: The number of calls grows very fast, almost doubling with each increase in n.
Time Complexity: O(2^n)
This means the work doubles roughly every time n increases by one, making it very slow for large n.
[X] Wrong: "Since fib calls itself twice, the time is just twice the time for n-1."
[OK] Correct: Each call creates two new calls, and those calls also create two calls each, so the total grows exponentially, not linearly.
Understanding overlapping subproblems helps you spot when to use memoization or dynamic programming to save time and avoid repeated work.
"What if we store results of fib calls in a cache to reuse them? How would the time complexity change?"
Practice
overlapping subproblems property in dynamic programming?Solution
Step 1: Understand overlapping subproblems
Overlapping subproblems means the same smaller problems appear multiple times during solving.Step 2: Match definition to options
The problem can be broken down into smaller subproblems that are solved multiple times. correctly states that subproblems repeat and can be reused to save time.Final Answer:
The problem can be broken down into smaller subproblems that are solved multiple times. -> Option AQuick Check:
Overlapping subproblems = repeated smaller problems [OK]
- Confusing overlapping subproblems with unique subproblems
- Thinking sorting is required for overlapping subproblems
- Assuming recursion alone solves overlapping subproblems
Solution
Step 1: Identify correct parameter passing for memo
Memo must be mutable reference to allow updates during recursion.Step 2: Check options for mutable reference
fn fib(n: usize, memo: &mut Vec) -> usize { ... } uses &mut Vec, which allows updating memo inside the function.Final Answer:
fn fib(n: usize, memo: &mut Vec) -> usize { ... } -> Option DQuick Check:
Mutable reference needed for memo updates [OK]
- Passing memo by value causing copies
- Using immutable reference &Vec which disallows updates
- Initializing memo inside function losing previous results
fn fib(n: usize, memo: &mut Vec) -> usize {
if n <= 1 { return n; }
if let Some(val) = memo[n] { return val; }
let val = fib(n-1, memo) + fib(n-2, memo);
memo[n] = Some(val);
val
}
fn main() {
let mut memo = vec![None; 6];
println!("{}", fib(5, &mut memo));
}Solution
Step 1: Trace fib(5) calls with memo
fib(5) = fib(4) + fib(3), fib(4) = 3, fib(3) = 2, so fib(5) = 5.Step 2: Confirm memo stores results to avoid recomputation
Memoization stores fib(3)=2, fib(4)=3, so final fib(5) = 5 is returned and printed.Final Answer:
5 -> Option BQuick Check:
fib(5) = 5 [OK]
- Confusing fib(5) with fib(6)
- Ignoring memoization effect
- Miscounting base cases
fn fib(n: usize, memo: &mut Vec) -> usize {
if n <= 1 { return n; }
if memo[n].is_some() { return memo[n].unwrap(); }
let val = fib(n-1, memo) + fib(n-2, memo);
memo[n] = Some(val);
val
}
fn main() {
let mut memo = vec![None; 5];
println!("{}", fib(5, &mut memo));
}Solution
Step 1: Check vector size for memo
Memo vector has size 5 but fib(5) accesses memo[5], which is out of bounds.Step 2: Understand Rust vector indexing
Indexing beyond vector length causes runtime panic, so vector size must be at least n+1.Final Answer:
Vector size is too small causing index out of bounds -> Option AQuick Check:
Memo size < n+1 causes index error [OK]
- Not allocating enough space for memo
- Ignoring Rust's zero-based indexing
- Assuming unwrap() always safe
Solution
Step 1: Understand minimum cost path problem
The cost to reach a cell depends on the best path to its neighbors (top or left).Step 2: Identify optimal substructure property
Optimal substructure means the optimal solution to the whole problem contains optimal solutions to subproblems.Final Answer:
Optimal substructure: the minimum cost path to a cell depends on minimum cost paths to adjacent cells. -> Option CQuick Check:
Minimum cost path uses optimal substructure [OK]
- Confusing overlapping subproblems with unique subproblems
- Assuming greedy always works here
- Thinking divide and conquer applies without overlap
Ready to prove it?
Pass the test and this pattern moves unknown
