Greedy vs DP: When to Use Each
Identify the hallmarks of problems solvable by greedy versus those requiring DP using the greedy-choice property and exchange argument.
Greedy vs DP: When to Use Each is a free DSA Interview Prep lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Greedy and DP Overview
Both Greedy and Dynamic Programming solve optimisation problems — finding a maximum, minimum, or optimal arrangement. Greedy makes the locally optimal choice at each step without reconsidering previous decisions. DP explores all possibilities but uses memoisation to avoid recomputation. Knowing which to apply can save hours of debugging an incorrect greedy or a needlessly complex DP table.
# Greedy: always take the locally best option
# Example: coin change with coins [1, 5, 10, 25]
# Greedy: take as many 25s as possible, then 10s, etc.
# This works for standard denominations but NOT all coin sets!
# DP: explore all possibilities via memoisation
# Example: coin change with coins [1, 3, 4] and target 6
# Greedy would pick 4, then 1, 1 → 3 coins
# DP finds: 3 + 3 → 2 coins (optimal!)
print('Greedy can fail when local optimum != global optimum')The Greedy Choice Property
A problem has the greedy choice property when a globally optimal solution can always be constructed by making locally optimal (greedy) choices. Formally: there exists an optimal solution that begins with the greedy choice, so we never need to backtrack. Proving this typically uses an exchange argument: assume any optimal solution does not include the greedy choice, then show you can swap it in without making things worse.
# Exchange argument example: Activity Selection
# Greedy: always pick the activity that ends earliest
# Proof: suppose optimal solution starts with activity A (not earliest-ending)
# Let G be the earliest-ending activity.
# Replace A with G in the solution:
# - G ends no later than A, so G does not conflict with any activity A allowed
# - The solution remains valid with at least as many activities
# Therefore greedy choice (earliest end) is always safe.
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14)]
activities.sort(key=lambda x: x[1]) # sort by end time
print('Sorted by end:', activities[:4], '...')Optimal Substructure
Both greedy and DP require optimal substructure: the optimal solution to the full problem contains optimal solutions to sub-problems. The distinction is whether optimal sub-problem solutions can be determined greedily (without exploring all options) or require comparing multiple choices. If you make a choice and the remaining sub-problem is identical in structure, greedy works. If you must compare several choices, use DP.
# Greedy works: activity selection
# Making the greedy choice (earliest-ending) leaves a sub-problem
# that is structurally identical (activity selection on remaining activities)
# and the greedy choice for the sub-problem is still valid.
# DP needed: 0/1 knapsack
# After choosing to include/exclude item i, the remaining sub-problem
# depends on WHICH item we chose — different choices yield different sub-problems.
# No single greedy rule works for all inputs.
print('Greedy: sub-problem is unique after each choice')
print('DP: sub-problem depends on which choice was made')Overlapping Sub-Problems Signal DP
If the same sub-problem is solved multiple times in a recursive decomposition, DP with memoisation is needed. Draw the recursion tree and look for repeated nodes. For Fibonacci, fib(3) is computed twice in the tree for fib(5). For coin change with coins [1,3,4] and target 6: sub-problems for target 3, 2, 1 appear multiple times. Overlapping sub-problems plus optimal substructure = DP.
# Recursion tree for coin change [1,3,4], target=6
# bt(6) → bt(5) → bt(4) → bt(3) (repeated!)
# → bt(2) → bt(1) (repeated!)
# → bt(3) (repeated!)
# → bt(2) (repeated!)
# Without memoisation: exponential time
# With DP table: O(target * len(coins)) time
def coin_change_dp(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
print(coin_change_dp([1, 3, 4], 6)) # 2 (3+3)
print(coin_change_dp([2], 3)) # -1 (impossible)Classic Greedy Problems
Problems where greedy is provably correct: (1) Activity/Interval Scheduling — earliest-finish-time greedy. (2) Minimum Spanning Tree — Prim's and Kruskal's algorithms. (3) Huffman Encoding — always merge the two lowest-frequency nodes. (4) Fractional Knapsack — take items by highest value/weight ratio. (5) Jump Game — track the maximum reachable index. All of these have proof-by-exchange-argument justifications.
# Fractional Knapsack: greedy works
def fractional_knapsack(items, capacity):
# Sort by value/weight ratio descending
items.sort(key=lambda x: x[1]/x[0], reverse=True)
total = 0
for weight, value in items:
if capacity <= 0: break
take = min(weight, capacity)
total += take * (value / weight)
capacity -= take
return total
items = [(10, 60), (20, 100), (30, 120)] # (weight, value)
print(fractional_knapsack(items, 50)) # 240.0
# 0/1 Knapsack: greedy FAILS
# Must use DP (can't take fractions)When Greedy Fails: Counterexamples
Finding a counterexample is the fastest way to disprove a greedy hypothesis. For coin change with coins [1, 3, 4] and target 6: greedy (largest first) takes 4, then 1+1 = 3 coins. DP finds 3+3 = 2 coins. For 0/1 knapsack: greedy by ratio takes the best-ratio item but may miss combinations that fill the capacity better. If you can construct a counterexample in under a minute, switch to DP.
# Counterexample: coin change with non-standard coins
def greedy_coins(coins, amount):
coins.sort(reverse=True)
count = 0
for c in coins:
while amount >= c:
amount -= c
count += 1
return count if amount == 0 else -1
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c] + 1)
return dp[amount] if dp[amount] < float('inf') else -1
coins, target = [1, 3, 4], 6
print('Greedy:', greedy_coins(coins[:], target)) # 3 (4+1+1)
print('DP: ', dp_coins(coins, target)) # 2 (3+3)Comparison Table: Greedy vs DP
Key differences side by side: Time complexity — greedy typically O(n log n) (dominated by sorting); DP is O(n × states). Space complexity — greedy O(1) auxiliary; DP O(states). Correctness — greedy needs proof; DP is always correct if states and recurrence are right. Applicability — greedy for scheduling, spanning trees, Huffman; DP for knapsack, sequence alignment, shortest path with negative weights.
# Performance comparison
import time
def time_it(func, *args):
start = time.time()
result = func(*args)
return result, time.time() - start
# Large coin change test
coins = [1, 5, 10, 25, 100]
amount = 10000
def dp_coins(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a-c]+1)
return dp[amount]
result, elapsed = time_it(dp_coins, coins, amount)
print(f'DP coin change(amount={amount}): {result} coins in {elapsed:.4f}s')Decision Framework
Interview decision flowchart: (1) Can you prove the greedy choice property with an exchange argument? If yes → greedy. (2) Do sub-problems overlap (same state reached multiple ways)? If yes → DP. (3) Does the problem ask for count or enumerate all solutions? → DP or backtracking. (4) Is the problem asking for a single optimal value with a natural ordering? Suspect greedy. (5) When in doubt, code the DP — it is always correct if the recurrence is right, even if slower.
# Decision questions to ask:
questions = [
'1. Is there a natural ordering (by time, ratio, size)?',
'2. Does making the greedy choice leave a smaller same-type problem?',
'3. Can I construct a counterexample quickly?',
'4. Are sub-problems reused across different choice sequences?',
'5. Does the problem involve counting or listing (not just optimising)?',
]
for q in questions:
print(q)
print()
print('Greedy signals: scheduling, spanning tree, Huffman, jump game')
print('DP signals: knapsack, edit distance, LCS, coin change (general)')Interval Problems: Greedy vs DP
Interval problems split between greedy and DP. Non-overlapping intervals (remove fewest): sort by end time, greedily take intervals — greedy is provably optimal. Weighted interval scheduling (maximise total weight): DP is needed because heavy intervals may overlap many light ones, requiring comparison of all valid subsets. The distinguishing factor is whether all intervals are equal weight (greedy) or variable weight (DP).
# Non-overlapping intervals: greedy works
def erase_overlap_intervals(intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end:
last_end = end # keep this interval
else:
count += 1 # remove this interval
return count
print(erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]])) # 1
print(erase_overlap_intervals([[1,2],[1,2],[1,2]])) # 2Recognising Problem Signals
Common problem statement signals: 'minimum number of operations', 'maximum profit', 'optimal selection' → could be greedy or DP, check overlap. 'count the number of ways' → always DP. 'find any valid schedule' → could be greedy. 'all possible' → backtracking. 'cannot take adjacent' → DP (house robber). 'meetings, intervals, tasks' → likely greedy. Mapping signals to algorithm families speeds up interview problem diagnosis.
# Signal-to-algorithm mapping
signals = {
'minimum steps/coins/operations': 'DP (unless trivially greedy)',
'maximum profit/value with constraint': 'DP (knapsack family)',
'count ways to reach/achieve': 'DP (always)',
'all combinations/permutations': 'Backtracking',
'schedule tasks within time': 'Greedy (sort by deadline/end)',
'cannot pick adjacent': 'DP (house robber pattern)',
'free to pick any subset': 'DP or Greedy (check overlap)',
'interval merging/selecting': 'Greedy (sort by end time)',
}
for signal, algo in signals.items():
print(f'{signal!r}: → {algo}')Proving Greedy Correctness
To prove a greedy algorithm correct, use the exchange argument: (1) Assume there is an optimal solution OPT that differs from the greedy solution G at the first choice. (2) Show you can swap the greedy choice into OPT without increasing the objective value. (3) By induction, the greedy solution is as good as any optimal solution. In interviews, you don't need a full proof, but explaining the exchange argument intuition shows deep understanding.
# Exchange argument demo: earliest-finish-time activity selection
# Suppose OPT starts with activity A (not earliest-ending)
# Let G = earliest-ending activity available
# A.end >= G.end (G ends earlier or same time)
# Swap A for G in OPT:
# - G.end <= A.end, so G does not conflict with anything A allowed after it
# - OPT remains valid with the same number of activities
# - Repeat: after swap, OPT begins with G, matching greedy first choice
# By induction, OPT can be transformed to match G activity by activity
# without losing activities → greedy is optimal
print('Exchange argument: any OPT can be modified to match Greedy without loss')
print('This proves Greedy >= OPT in objective value')Quick Check
Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.
Lesson Recap
In this lesson you learned: greedy is correct when the greedy choice property holds — provable via exchange argument, DP is needed when sub-problems overlap (same sub-problem reached multiple ways) and cannot be resolved by a single greedy rule, and the fastest way to disprove a greedy hypothesis is to construct a counterexample with non-standard inputs. Next up we solve Interval Scheduling and Merging using the greedy sort-by-end-time approach.
Frequently asked questions
Is the “Greedy vs DP: When to Use Each” lesson free?
Yes — the full text of “Greedy vs DP: When to Use Each” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.
What will I learn in “Greedy vs DP: When to Use Each”?
Identify the hallmarks of problems solvable by greedy versus those requiring DP using the greedy-choice property and exchange argument. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DSA Interview Prep?
No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Greedy vs DP: When to Use Each” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DSA Interview Prep lesson?
Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Greedy vs DP: When to Use Each
- Interval Scheduling and Merging
- Jump Game I and II
- Task Scheduler and Gas Station