Overlapping Subproblems and Optimal Substructure in DSA Go - Time & Space Complexity
We want to understand how solving problems with repeated parts affects the time it takes.
How does breaking a problem into smaller parts and reusing answers change the work needed?
Analyze the time complexity of the following code snippet.
func fib(n int, memo map[int]int) int {
if n <= 1 {
return n
}
if val, ok := memo[n]; ok {
return val
}
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
}
This code calculates the nth Fibonacci number using a technique that saves results to avoid repeated work.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls to fib with smaller numbers.
- How many times: Each unique number from 0 to n is calculated once and saved.
As n grows, the number of unique calculations grows roughly in a straight line.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 calculations |
| 100 | About 100 calculations |
| 1000 | About 1000 calculations |
Pattern observation: The work grows directly with n, not exponentially.
Time Complexity: O(n)
This means the time to solve grows in a straight line with the input size because each subproblem is solved once.
[X] Wrong: "Recursive Fibonacci always takes exponential time because it calls itself twice each time."
[OK] Correct: Using saved answers (memoization) means each number is calculated once, avoiding repeated work and reducing time to linear.
Understanding how to spot repeated work and reuse answers is a key skill that shows you can write efficient code for complex problems.
"What if we removed the memo map and calculated fib(n) with plain recursion? 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 the solution process.Step 2: Match the definition with options
The problem can be broken down into smaller subproblems which are solved multiple times. correctly states that subproblems are solved multiple times, which is the key idea behind overlapping subproblems.Final Answer:
The problem can be broken down into smaller subproblems which are solved multiple times. -> Option CQuick 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 map declaration for memoization
Memoization requires a map with keys and values. Here keys and values are integers.Step 2: Check each option
memo := make(map[int]int) uses make(map[int]int) which creates a map with int keys and int values, perfect for memoization.Final Answer:
memo := make(map[int]int) -> Option DQuick Check:
Map[int]int for memoization = memo := make(map[int]int) [OK]
- Using slices instead of maps for memoization
- Using wrong key types like string when int is needed
- Confusing slice initialization with map initialization
package main
import "fmt"
func fib(n int, memo map[int]int) int {
if n <= 1 {
return n
}
if val, ok := memo[n]; ok {
return val
}
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
}
func main() {
memo := make(map[int]int)
fmt.Println(fib(5, memo))
}Solution
Step 1: Understand Fibonacci calculation with memo
fib(5) = fib(4) + fib(3), and so on, with memo storing results to avoid repeats.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 AQuick Check:
fib(5) = 5 [OK]
- Confusing fib(5) with fib(6)
- Ignoring memoization effect and recalculating
- Mixing up base cases n<=1
func fib(n int, memo map[int]int) int {
if n == 0 {
return 0
}
if n == 1 {
return 1
}
if val, ok := memo[n]; ok {
return val
}
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
}Solution
Step 1: Check base cases and memo usage
Base cases for n==0 and n==1 are correctly handled. Memo is used properly inside function.Step 2: Identify initialization issue
The function expects memo map passed in, but if not initialized before calling, it causes runtime error.Final Answer:
Memo map is not initialized before use -> Option AQuick Check:
Uninitialized map causes runtime panic [OK]
- Assuming base cases are wrong when they are correct
- Forgetting to initialize memo map before first call
- Missing return statement after memo assignment
cost := [][]int{
{1, 3, 5},
{2, 1, 2},
{4, 3, 1},
}
Which approach best uses Overlapping Subproblems and Optimal Substructure to solve this efficiently?Solution
Step 1: Understand problem properties
Minimum cost path has optimal substructure: minimum cost to a cell depends on minimum cost to neighbors.Step 2: Choose approach using overlapping subproblems
Dynamic programming with a 2D memo table stores results of subproblems (cells), avoiding repeated calculations.Final Answer:
Use dynamic programming with a 2D memo table storing minimum cost to each cell -> Option BQuick Check:
DP with memo table = efficient solution [OK]
- Trying recursion without memo causes exponential time
- Sorting grid ignores path constraints
- Greedy approach may miss global minimum path
Ready to prove it?
Pass the test and this pattern moves unknown
