DP vs Recursion vs Greedy Choosing the Right Tool in DSA C# - Complexity Comparison
We want to understand how different problem-solving methods affect the time it takes to run code.
Which method grows faster or slower as the input gets bigger?
Analyze the time complexity of the following code snippet.
// Recursive Fibonacci (simple recursion)
int FibRec(int n) {
if (n <= 1) return n;
return FibRec(n - 1) + FibRec(n - 2);
}
// Dynamic Programming Fibonacci (memoization)
int FibDP(int n, int[] memo) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
memo[n] = FibDP(n - 1, memo) + FibDP(n - 2, memo);
return memo[n];
}
// Greedy example: Coin change (not always optimal)
int GreedyCoinChange(int amount, int[] coins) {
int count = 0;
for (int i = coins.Length - 1; i >= 0; i--) {
count += amount / coins[i];
amount %= coins[i];
}
return count;
}
This code shows three ways to solve problems: simple recursion, dynamic programming with memory, and a greedy approach.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls in FibRec, repeated subproblems in FibDP, and loop over coins in GreedyCoinChange.
- How many times: FibRec calls itself many times exponentially; FibDP solves each subproblem once; Greedy loops once over coins.
As input size grows, recursive calls in FibRec explode quickly, DP grows slowly, and Greedy stays fast.
| Input Size (n) | Approx. Operations FibRec | Approx. Operations FibDP | Approx. Operations Greedy |
|---|---|---|---|
| 10 | ~177 calls | 10 calls | 3 loops |
| 20 | ~21,891 calls | 20 calls | 3 loops |
| 30 | ~2,692,537 calls | 30 calls | 3 loops |
Pattern observation: Simple recursion grows very fast, DP grows linearly, and Greedy grows with coin types, not input size.
Time Complexity: O(2^n) for Recursion, O(n) for DP, O(k) for Greedy
Recursion can be very slow, DP is efficient by remembering results, and Greedy is fast but may not always be correct.
[X] Wrong: "Greedy always gives the best answer quickly."
[OK] Correct: Greedy picks the best choice now but can miss better overall solutions, so it's not always correct.
Knowing when to use recursion, DP, or greedy helps you solve problems efficiently and impress interviewers with smart choices.
"What if we added memoization to the greedy coin change? How would the time complexity change?"
Practice
Solution
Step 1: Understand problem characteristics
Overlapping subproblems mean the same smaller problems repeat. Optimal substructure means the best solution can be built from best solutions of smaller problems.Step 2: Match approach to characteristics
Dynamic Programming stores results of subproblems to avoid recomputation, ideal for overlapping subproblems and optimal substructure.Final Answer:
Dynamic Programming -> Option BQuick Check:
Overlapping subproblems + optimal substructure = Dynamic Programming [OK]
- Choosing greedy when subproblems overlap
- Using simple recursion without memoization
- Confusing optimal substructure with greedy choice
Solution
Step 1: Understand greedy activity selection logic
We select the first activity, then pick next activities starting after the last selected activity ends.Step 2: Check code correctness
int count = 1; int lastEnd = end[0]; for (int i = 1; i < start.Length; i++) { if (start[i] >= lastEnd) { count++; lastEnd = end[i]; } } Console.WriteLine(count); initializes count to 1, tracks lastEnd, and increments count only if start[i] >= lastEnd, updating lastEnd. This matches greedy logic.Final Answer:
the code that tracks lastEnd -> Option AQuick Check:
Greedy activity selection code = int count = 1; int lastEnd = end[0]; for (int i = 1; i < start.Length; i++) { if (start[i] >= lastEnd) { count++; lastEnd = end[i]; } } Console.WriteLine(count); [OK]
- Not sorting activities by end time before selection
- Comparing start[i] with wrong end time
- Counting activities incorrectly
int Fib(int n) { if (n <= 1) return n; return Fib(n - 1) + Fib(n - 2); } Console.WriteLine(Fib(5));Solution
Step 1: Trace recursive calls for Fib(5)
Fib(5) = Fib(4) + Fib(3); Fib(4) = Fib(3) + Fib(2); Fib(3) = Fib(2) + Fib(1); Fib(2) = Fib(1) + Fib(0); Fib(1)=1, Fib(0)=0.Step 2: Calculate values bottom-up
Fib(0)=0, Fib(1)=1, Fib(2)=1, Fib(3)=2, Fib(4)=3, Fib(5)=5.Final Answer:
5 -> Option CQuick Check:
Fibonacci(5) = 5 [OK]
- Confusing index starting at 0 or 1
- Miscomputing recursive sums
- Assuming memoization changes output
int CoinChange(int[] coins, int amount) { if (amount == 0) return 0; if (amount < 0) return -1; int min = int.MaxValue; foreach (int coin in coins) { int res = CoinChange(coins, amount - coin); if (res >= 0 && res < min) min = 1 + res; } return min == int.MaxValue ? -1 : min; }Solution
Step 1: Analyze recursion and base cases
Base cases are correct: 0 returns 0, negative returns -1. The recursion tries all coins.Step 2: Identify missing optimization
Without memoization, the function recalculates subproblems many times, causing inefficiency and possible stack overflow.Final Answer:
No memoization causing repeated calculations -> Option AQuick Check:
Missing memoization = No memoization causing repeated calculations [OK]
- Thinking base cases are wrong
- Believing loop type causes bug
- Ignoring performance issues
Solution
Step 1: Evaluate greedy approach limitations
Greedy works only if coin denominations are canonical (like US coins). Otherwise, it can fail to find minimum coins.Step 2: Consider recursion and iteration drawbacks
Recursion without memoization repeats work; simple iteration without storing results misses subproblem reuse.Step 3: Understand dynamic programming benefits
DP with bottom-up tabulation stores minimum coins for all amounts up to target, guaranteeing optimal and efficient solution.Final Answer:
Dynamic programming ensures optimal solution efficiently -> Option DQuick Check:
Minimum coins problem = DP bottom-up [OK]
- Assuming greedy always works
- Ignoring performance of recursion
- Not storing intermediate results
Ready to prove it?
Pass the test and this pattern moves unknown
