Bird
Raised Fist0
DSA Rustprogramming

Overlapping Subproblems and Optimal Substructure in DSA Rust

Choose your learning style10 modes available
Mental Model
Some problems can be broken into smaller parts that repeat, and the best solution uses the best answers to those parts.
Analogy: Imagine building a big Lego castle by reusing the same small Lego pieces many times instead of making new ones each time.
Problem
  ↓
Subproblem A -> Subproblem B -> Subproblem C
  ↓           ↓
Subproblem A (reused)  Subproblem B (reused)
Dry Run Walkthrough
Input: Calculate Fibonacci number for 5 using recursion
Goal: Show how the same smaller problems are solved multiple times and how using their answers saves work
Step 1: Calculate fib(5) by calculating fib(4) and fib(3)
fib(5) -> fib(4), fib(3)
Why: fib(5) depends on fib(4) and fib(3)
Step 2: Calculate fib(4) by calculating fib(3) and fib(2)
fib(5) -> fib(4) -> fib(3), fib(2); fib(3)
Why: fib(4) depends on fib(3) and fib(2)
Step 3: Calculate fib(3) by calculating fib(2) and fib(1)
fib(5) -> fib(4) -> fib(3) -> fib(2), fib(1); fib(3) -> fib(2), fib(1)
Why: fib(3) depends on fib(2) and fib(1)
Step 4: Notice fib(3) is calculated twice, so reuse the answer instead of recalculating
fib(5) -> fib(4) -> fib(3) (reused), fib(2); fib(3) (reused)
Why: Avoid repeating work by reusing answers to overlapping subproblems
Step 5: Combine answers from smaller problems to get fib(5)
fib(5) = fib(4) + fib(3) = 3 + 2 = 5
Why: Optimal substructure means the best answer uses best answers to parts
Result:
fib(5) = 5
Annotated Code
DSA Rust
fn fib(n: usize, memo: &mut Vec<Option<usize>>) -> usize {
    if let Some(value) = memo[n] {
        return value; // reuse stored answer to avoid repeated work
    }
    if n <= 1 {
        memo[n] = Some(n); // base case: fib(0)=0, fib(1)=1
        return n;
    }
    let result = fib(n - 1, memo) + fib(n - 2, memo); // combine answers of subproblems
    memo[n] = Some(result); // store answer for reuse
    result
}

fn main() {
    let n = 5;
    let mut memo = vec![None; n + 1];
    let result = fib(n, &mut memo);
    println!("fib({}) = {}", n, result);
}
if let Some(value) = memo[n] {
reuse stored answer to avoid repeated work
if n <= 1 {
handle base cases fib(0)=0 and fib(1)=1
let result = fib(n - 1, memo) + fib(n - 2, memo);
combine answers of smaller subproblems
memo[n] = Some(result);
store answer for reuse in overlapping subproblems
OutputSuccess
fib(5) = 5
Complexity Analysis
Time: O(n) because each subproblem fib(k) is solved once and reused
Space: O(n) for the memo array storing answers to subproblems
vs Alternative: Naive recursion is O(2^n) because it recalculates the same subproblems many times; memoization reduces this to O(n)
Edge Cases
n = 0 or n = 1
Returns 0 or 1 immediately as base cases
DSA Rust
if n <= 1 { memo[n] = Some(n); return n; }
Large n with memoization
Efficiently computes fib(n) without repeated work
DSA Rust
if let Some(value) = memo[n] { return value; }
When to Use This Pattern
When a problem asks for a solution that can be built from smaller repeated parts, look for overlapping subproblems and optimal substructure to use memoization or dynamic programming.
Common Mistakes
Mistake: Not storing answers to subproblems, causing repeated calculations
Fix: Use a memo structure to save and reuse answers for overlapping subproblems
Mistake: Missing base cases leading to infinite recursion
Fix: Add base cases to stop recursion at smallest subproblems
Summary
It breaks problems into smaller parts that repeat and uses best answers to those parts to build the final solution.
Use it when a problem can be divided into overlapping smaller problems with optimal substructure.
The key is to save answers to repeated parts so you don't do the same work twice.

Practice

(1/5)
1. Which of the following best describes the overlapping subproblems property in dynamic programming?
easy
A. The problem can be broken down into smaller subproblems that are solved multiple times.
B. The problem has a unique solution without repeating any subproblems.
C. The problem requires sorting before solving.
D. The problem can only be solved using recursion without memoization.

Solution

  1. Step 1: Understand overlapping subproblems

    Overlapping subproblems means the same smaller problems appear multiple times during solving.
  2. 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.
  3. Final Answer:

    The problem can be broken down into smaller subproblems that are solved multiple times. -> Option A
  4. Quick Check:

    Overlapping subproblems = repeated smaller problems [OK]
Hint: Look for repeated smaller problems solved multiple times [OK]
Common Mistakes:
  • Confusing overlapping subproblems with unique subproblems
  • Thinking sorting is required for overlapping subproblems
  • Assuming recursion alone solves overlapping subproblems
2. Which Rust syntax correctly declares a function to compute Fibonacci numbers using memoization?
easy
A. fn fib(n: usize, memo: &Vec<usize>) -> usize { ... }
B. fn fib(n: usize) -> usize { let memo = Vec::new(); ... }
C. fn fib(n: usize, memo: Vec<usize>) -> usize { ... }
D. fn fib(n: usize, memo: &mut Vec) -> usize { ... }

Solution

  1. Step 1: Identify correct parameter passing for memo

    Memo must be mutable reference to allow updates during recursion.
  2. 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.
  3. Final Answer:

    fn fib(n: usize, memo: &mut Vec) -> usize { ... } -> Option D
  4. Quick Check:

    Mutable reference needed for memo updates [OK]
Hint: Use &mut for mutable references in Rust function parameters [OK]
Common Mistakes:
  • Passing memo by value causing copies
  • Using immutable reference &Vec which disallows updates
  • Initializing memo inside function losing previous results
3. What is the output of this Rust code that uses memoization for Fibonacci?
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));
}
medium
A. 8
B. 5
C. 3
D. 13

Solution

  1. Step 1: Trace fib(5) calls with memo

    fib(5) = fib(4) + fib(3), fib(4) = 3, fib(3) = 2, so fib(5) = 5.
  2. 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.
  3. Final Answer:

    5 -> Option B
  4. Quick Check:

    fib(5) = 5 [OK]
Hint: Recall Fibonacci sequence: 0,1,1,2,3,5,8... [OK]
Common Mistakes:
  • Confusing fib(5) with fib(6)
  • Ignoring memoization effect
  • Miscounting base cases
4. Identify the bug in this Rust code for Fibonacci with memoization:
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));
}
medium
A. Vector size is too small causing index out of bounds
B. Memo is not mutable
C. Base case condition is incorrect
D. Using unwrap() causes panic always

Solution

  1. Step 1: Check vector size for memo

    Memo vector has size 5 but fib(5) accesses memo[5], which is out of bounds.
  2. Step 2: Understand Rust vector indexing

    Indexing beyond vector length causes runtime panic, so vector size must be at least n+1.
  3. Final Answer:

    Vector size is too small causing index out of bounds -> Option A
  4. Quick Check:

    Memo size < n+1 causes index error [OK]
Hint: Ensure memo vector size is n+1 for fib(n) [OK]
Common Mistakes:
  • Not allocating enough space for memo
  • Ignoring Rust's zero-based indexing
  • Assuming unwrap() always safe
5. You want to find the minimum cost path in a grid from top-left to bottom-right, moving only right or down. Which property allows using dynamic programming to solve this efficiently?
hard
A. Greedy choice: always pick the cell with the smallest immediate cost.
B. Overlapping subproblems: each cell's cost is unique and never reused.
C. Optimal substructure: the minimum cost path to a cell depends on minimum cost paths to adjacent cells.
D. Divide and conquer: split the grid into independent parts solved separately.

Solution

  1. Step 1: Understand minimum cost path problem

    The cost to reach a cell depends on the best path to its neighbors (top or left).
  2. Step 2: Identify optimal substructure property

    Optimal substructure means the optimal solution to the whole problem contains optimal solutions to subproblems.
  3. Final Answer:

    Optimal substructure: the minimum cost path to a cell depends on minimum cost paths to adjacent cells. -> Option C
  4. Quick Check:

    Minimum cost path uses optimal substructure [OK]
Hint: Look for solutions built from optimal subproblems [OK]
Common Mistakes:
  • Confusing overlapping subproblems with unique subproblems
  • Assuming greedy always works here
  • Thinking divide and conquer applies without overlap
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard