Memoization Top Down DP in DSA Rust - Time & Space Complexity
We analyze how memoization in top-down dynamic programming affects the time taken by recursive solutions.
We want to know how saving results changes the number of repeated calculations.
Analyze the time complexity of the following code snippet.
fn fib_memo(n: usize, memo: &mut Vec<Option<usize>>) -> usize {
if n <= 1 {
return n;
}
if let Some(val) = memo[n] {
return val;
}
let result = fib_memo(n - 1, memo) + fib_memo(n - 2, memo);
memo[n] = Some(result);
result
}
This code calculates the nth Fibonacci number using recursion with memoization to save results.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls to fib_memo for smaller n values.
- How many times: Each unique n from 0 to n is computed once and then reused from memo.
Each number from 0 up to n is calculated once, then saved to avoid repeats.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 calculations |
| 100 | About 100 calculations |
| 1000 | About 1000 calculations |
Pattern observation: The number of operations grows roughly in a straight line with n.
Time Complexity: O(n)
This means the time grows directly with the input size because each subproblem is solved once.
[X] Wrong: "Memoization still causes exponential time because of recursion."
[OK] Correct: Memoization stores results, so each subproblem runs once, avoiding repeated work.
Understanding memoization shows you how to turn slow recursive solutions into efficient ones, a key skill in problem solving.
"What if we removed memoization and only used plain recursion? How would the time complexity change?"
Practice
Solution
Step 1: Understand memoization role
Memoization saves results of subproblems so they are not recalculated multiple times.Step 2: Compare options
Only To store results of subproblems to avoid repeated calculations describes this purpose correctly. Sorting, converting recursion, or deleting variables are unrelated.Final Answer:
To store results of subproblems to avoid repeated calculations -> Option AQuick Check:
Memoization = store subproblem results [OK]
- Confusing memoization with sorting
- Thinking memoization reduces memory by deleting data
- Believing memoization changes recursion to iteration
Solution
Step 1: Identify correct memo type
Memoization cache should store Option<usize> to mark computed or not, initialized with None.Step 2: Check each option
let mut memo: Vec = vec![None; n + 1]; uses Vec with None values, correct for memoization. let memo = HashMap::new(); is a HashMap but missing mut and initialization syntax. let memo: Vec<usize> = Vec::new(); lacks Option type. let mut memo: Vec<usize> = vec![0; n]; uses usize with zeros, which can't distinguish computed or not.Final Answer:
let mut memo: Vec = vec![None; n + 1]; -> Option CQuick Check:
Memo cache = Vec with None [OK]
- Using Vec<usize> without Option to track computed
- Not making memo mutable
- Incorrect initialization of HashMap
fn fib(n: usize, memo: &mut Vec) -> usize {
if n <= 1 {
return n;
}
if let Some(val) = memo[n] {
return val;
}
let result = fib(n - 1, memo) + fib(n - 2, memo);
memo[n] = Some(result);
result
}
fn main() {
let n = 5;
let mut memo = vec![None; n + 1];
println!("{}", fib(n, &mut memo));
}What is the output when this code runs?
Solution
Step 1: Understand Fibonacci calculation
fib(5) = fib(4) + fib(3). Calculating recursively with memoization avoids repeated work.Step 2: Calculate fib(5)
fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, fib(4)=3, fib(5)=5.Final Answer:
5 -> Option BQuick Check:
fib(5) = 5 [OK]
- Confusing fib(5) with fib(6)
- Forgetting zero-based indexing
- Ignoring memoization effect
fn fact(n: usize, memo: &mut Vec) -> usize {
if n == 0 {
return 1;
}
if memo[n].is_some() {
return memo[n].unwrap();
}
let result = n * fact(n - 1, memo);
memo[n] = Some(result);
result
}
fn main() {
let n = 5;
let mut memo = vec![None; n];
println!("{}", fact(n, &mut memo));
}What is the bug causing a runtime error?
Solution
Step 1: Check memo vector size
Memo is created with size n (5), but fact accesses memo[n], which is memo[5], out of bounds since indices go 0 to 4.Step 2: Identify cause of runtime error
Accessing memo[5] causes panic due to index out of bounds. The vector should be size n+1 to include index 5.Final Answer:
The memo vector size is too small causing index out of bounds -> Option AQuick Check:
Vector size < needed index = runtime error [OK]
- Ignoring vector size when indexing
- Changing base case incorrectly
- Thinking recursion is the problem
n steps, taking 1 or 2 steps at a time. Which of these implementations correctly applies memoization to avoid repeated calculations?fn climb(n: usize, memo: &mut Vec) -> usize {
if n == 0 {
return 1;
}
if n < 0 {
return 0;
}
if let Some(val) = memo[n] {
return val;
}
let ways = climb(n - 1, memo) + climb(n - 2, memo);
memo[n] = Some(ways);
ways
}
fn main() {
let n = 4;
let mut memo = vec![None; n + 1];
println!("{}", climb(n, &mut memo));
}Solution
Step 1: Check base cases and recursion
Base cases handle n == 0 (1 way) and n < 0 (0 ways), which is correct for counting ways.Step 2: Verify memoization usage
Memo vector is mutable and stores Option<usize>. It checks memo[n] before recursion and stores result after calculation.Step 3: Confirm no unsafe indexing
Negative n returns 0 before indexing memo, so no out-of-bounds access.Final Answer:
Correct implementation with proper base cases and memo usage -> Option DQuick Check:
Base cases + memo check = correct memoization [OK]
- Not handling negative n before memo access
- Forgetting to make memo mutable
- Using iteration instead of recursion in top-down DP
Ready to prove it?
Pass the test and this pattern moves unknown
