Greedy Algorithms: When Local Optimum = Global Optimum
Learn when and why greedy works — interval scheduling, activity selection, Huffman coding, and the proof techniques that validate greedy choices.
When Does Greedy Work?
Greedy algorithms make the locally optimal choice at each step, hoping it leads to a globally optimal solution. This works when the problem has the greedy-choice property (a local optimum is part of a global optimum) and optimal substructure. The hardest part is recognizing when greedy applies and proving correctness.
Greedy vs DP: if making the best local choice always leads to the global optimum, use greedy (faster, simpler). If you need to consider multiple choices and their consequences, use DP. When in doubt, try greedy first — if you can construct a counterexample, switch to DP.
Interval Scheduling
Given intervals, find the maximum number of non-overlapping intervals. The greedy strategy: sort by end time, always pick the interval that ends earliest. This leaves the most room for future intervals.
def max_non_overlapping(intervals: list[list[int]]) -> int:
"""Maximum number of non-overlapping intervals."""
intervals.sort(key=lambda x: x[1]) # Sort by end time
count = 0
prev_end = float("-inf")
for start, end in intervals:
if start >= prev_end:
count += 1
prev_end = end
return count
def min_intervals_to_remove(intervals: list[list[int]]) -> int:
"""Minimum removals to make all intervals non-overlapping."""
return len(intervals) - max_non_overlapping(intervals)Merge Intervals
def merge_intervals(intervals: list[list[int]]) -> list[list[int]]:
"""Merge all overlapping intervals."""
intervals.sort()
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return mergedJump Game
At each position, you can jump up to nums[i] steps. Can you reach the end? Greedy approach: track the farthest reachable position. If you ever can't advance past your current position, you're stuck.
def can_jump(nums: list[int]) -> bool:
"""Can you reach the last index?"""
farthest = 0
for i in range(len(nums)):
if i > farthest:
return False
farthest = max(farthest, i + nums[i])
return True
def min_jumps(nums: list[int]) -> int:
"""Minimum jumps to reach the end."""
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumpsTask Scheduling
def task_scheduler(tasks: list[str], n: int) -> int:
"""Minimum intervals to execute all tasks with cooldown n."""
from collections import Counter
freq = Counter(tasks)
max_freq = max(freq.values())
max_count = sum(1 for f in freq.values() if f == max_freq)
# Formula: (max_freq - 1) * (n + 1) + max_count
# But result can't be less than total tasks
return max(len(tasks), (max_freq - 1) * (n + 1) + max_count)Common greedy problems: interval scheduling, activity selection, Huffman coding, fractional knapsack, gas station, candy distribution, and jump game variants. For each, identify what to sort by and what greedy choice to make.