Greedy vs DP How to Know Which to Apply in DSA Python - Complexity Comparison
We want to understand how to decide between greedy and dynamic programming (DP) approaches by looking at their time costs.
Which method runs faster and when should we use each?
Analyze the time complexity of these two approaches solving the same problem.
# Greedy approach example
items = sorted(items, key=lambda x: x.value / x.weight, reverse=True)
result = 0
for item in items:
if capacity >= item.weight:
capacity -= item.weight
result += item.value
# Dynamic Programming approach example
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if items[i-1].weight <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w - items[i-1].weight] + items[i-1].value)
else:
dp[i][w] = dp[i-1][w]
The greedy code picks items by best value per weight quickly. The DP code tries all combinations to find the best total value.
Look at the loops and repeated steps in each approach.
- Greedy primary operation: Sorting items once, then one loop over all items.
- Greedy loop count: Runs once over n items after sorting.
- DP primary operation: Nested loops over items and capacity values.
- DP loop count: Runs n times for items and capacity times for weight, so n * capacity.
As input size grows, how do the operations increase?
| Input Size (n) | Capacity (W) | Greedy Ops | DP Ops |
|---|---|---|---|
| 10 | 50 | ~10 + sorting | 10 * 50 = 500 |
| 100 | 1000 | ~100 + sorting | 100 * 1000 = 100,000 |
| 1000 | 10,000 | ~1000 + sorting | 1000 * 10,000 = 10,000,000 |
Greedy grows mostly with number of items and sorting cost. DP grows with both items and capacity, multiplying operations.
Time Complexity: O(n log n) for Greedy, O(n * W) for DP
Greedy is faster but less thorough; DP checks all possibilities and takes longer.
[X] Wrong: "Greedy always gives the best answer quickly."
[OK] Correct: Greedy works only when local choices lead to global best. Otherwise, DP is needed to explore all options.
Knowing when to pick greedy or DP shows you understand problem patterns and efficiency, a key skill in coding interviews.
"What if the capacity W is very large but the number of items n is small? How does that affect the time complexity choice between greedy and DP?"
Practice
Solution
Step 1: Understand greedy algorithm conditions
Greedy algorithms work when choosing the best option at each step leads to a global optimum, which means the problem has optimal substructure and no overlapping subproblems.Step 2: Compare with dynamic programming conditions
Dynamic programming is used when problems have overlapping subproblems and require storing intermediate results (memoization) to avoid repeated work.Final Answer:
When the problem has optimal substructure and no overlapping subproblems -> Option AQuick Check:
Greedy = optimal substructure + no overlap [OK]
- Confusing overlapping subproblems as a greedy condition
- Thinking greedy always finds the best solution
- Using dynamic programming when greedy suffices
Solution
Step 1: Identify the correct data structure for memoization
Memoization requires storing key-value pairs of subproblem results, so a dictionary (hash map) is appropriate.Step 2: Check the options
memo = {} initializes an empty dictionary, which is correct. Lists or sets do not store key-value pairs, and an integer cannot store multiple results.Final Answer:
memo = {} -> Option BQuick Check:
Memoization uses dictionary = {} [OK]
- Using list or set instead of dictionary for memo
- Initializing memo as a number
- Confusing data structures for memoization
intervals = [(1, 3), (2, 5), (4, 7)]
intervals.sort(key=lambda x: x[1])
selected = []
end = 0
for start, finish in intervals:
if start >= end:
selected.append((start, finish))
end = finish
print(selected)Solution
Step 1: Sort intervals by finish time
Sorted intervals: [(1, 3), (2, 5), (4, 7)]Step 2: Select intervals greedily
Start with end=0. (1,3) start=1 >=0, select it, end=3. Next (2,5) start=2 <3 skip. Next (4,7) start=4 >=3 select it.Final Answer:
[(1, 3), (4, 7)] -> Option CQuick Check:
Greedy interval selection = [(1, 3), (4, 7)] [OK]
- Not sorting intervals by finish time
- Selecting overlapping intervals
- Appending intervals without condition
def fib(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib(n-1) + fib(n-2)
return memo[n]
print(fib(5))Solution
Step 1: Identify the use of mutable default argument
The function uses memo={} as a default argument, which is mutable and shared across calls, causing unexpected behavior if fib is called multiple times.Step 2: Explain why this is a bug
This can cause memo to retain values from previous calls, leading to incorrect results or memory issues.Final Answer:
Mutable default argument causes incorrect memoization across calls -> Option AQuick Check:
Mutable default dict in function = bug [OK]
- Ignoring mutable default argument issue
- Thinking base cases are missing
- Assuming memo is not updated
Solution
Step 1: Understand greedy approach limitation
Greedy picks largest coin first, but for amount 6, greedy picks 4+1+1=3 coins, but optimal is 3+3=2 coins.Step 2: Recognize dynamic programming advantage
Dynamic programming tries all combinations and finds minimum coins, handling cases where greedy fails.Final Answer:
Dynamic programming is needed because greedy fails for some amounts -> Option DQuick Check:
Coin change needs DP, greedy can fail [OK]
- Assuming greedy always finds minimum coins
- Ignoring counterexamples where greedy fails
- Thinking coin change is always greedy
Ready to prove it?
Pass the test and this pattern moves unknown
