Greedy Algorithms Pattern¶
Greedy algorithms make locally optimal choices at each step with the hope of finding a global optimum. They build solutions piece by piece, always choosing the next piece that offers the most immediate benefit.
When to Use Greedy Algorithms¶
Use this pattern when:
- Problem has optimal substructure
- Greedy choice property holds (local optimum leads to global optimum)
- Activity selection or scheduling problems
- Minimum spanning tree problems
- Huffman coding or compression
- Fractional knapsack (not 0/1 knapsack)
- Problems involving intervals, meetings, or events
Core Approach¶
Greedy Choice Property¶
A greedy algorithm must satisfy: 1. Greedy Choice: Local optimum contributes to global optimum 2. Optimal Substructure: Optimal solution contains optimal subsolutions
Key Steps¶
- Sort: Often requires sorting by some criterion
- Select: Pick the locally best option
- Validate: Check if choice is feasible
- Repeat: Continue until solution complete
Greedy vs Dynamic Programming¶
-
Use Greedy when
- Local optimum always leads to global
- No overlapping subproblems to store
- Typically O(n log n) due to sorting
-
Use DP when
- Need to consider all possibilities
- Overlapping subproblems exist
- Greedy doesn't guarantee optimality
Problem 1: Jump Game¶
Difficulty: Medium
LeetCode: #55
Problem Statement¶
Given an array where each element represents your maximum jump length at that position, determine if you can reach the last index starting from the first index.
Example:
Input: nums = [2, 3, 1, 1, 4]
Output: true
Explanation: Jump 1 step from 0 to 1, then 3 steps to the last index
Input: nums = [3, 2, 1, 0, 4]
Output: false
Explanation: You will always arrive at index 3, which has 0 jump length
Approach¶
Greedy approach: Track maximum reachable index
- Start from position 0
- At each position, update farthest reachable index
- If current position exceeds farthest, can't proceed
- If farthest reaches last index, return true
Always choosing maximum reach ensures we explore all possibilities.
Solution¶
def can_jump(nums):
"""
Determine if can reach last index using greedy.
Time Complexity: O(n) - single pass
Space Complexity: O(1) - only tracking max reach
"""
max_reach = 0
for i in range(len(nums)):
# Can't reach this position
if i > max_reach:
return False
# Update maximum reachable position
max_reach = max(max_reach, i + nums[i])
# Reached or passed last index
if max_reach >= len(nums) - 1:
return True
return False
# Test
print(can_jump([2, 3, 1, 1, 4])) # Output: True
print(can_jump([3, 2, 1, 0, 4])) # Output: False
Step-by-Step Walkthrough¶
Example 1: nums = [2, 3, 1, 1, 4]
i=0, nums[0]=2:
max_reach = max(0, 0+2) = 2
Can reach up to index 2
i=1, nums[1]=3:
1 <= 2 (reachable)
max_reach = max(2, 1+3) = 4
Can reach up to index 4 (last index!)
Return True
Result: True
Example 2: nums = [3, 2, 1, 0, 4]
i=0, nums[0]=3:
max_reach = max(0, 0+3) = 3
Can reach up to index 3
i=1, nums[1]=2:
1 <= 3 (reachable)
max_reach = max(3, 1+2) = 3
Still max 3
i=2, nums[2]=1:
2 <= 3 (reachable)
max_reach = max(3, 2+1) = 3
Still max 3
i=3, nums[3]=0:
3 <= 3 (reachable)
max_reach = max(3, 3+0) = 3
Can't advance beyond 3
i=4:
4 > 3 (not reachable!)
Return False
Result: False
Problem 2: Meeting Rooms II¶
Difficulty: Medium
LeetCode: #253
Problem Statement¶
Given an array of meeting time intervals, find the minimum number of conference rooms required.
Example:
Input: intervals = [[0,30],[5,10],[15,20]]
Output: 2
Explanation: Meetings [5,10] and [15,20] overlap with [0,30]
Input: intervals = [[7,10],[2,4]]
Output: 1
Approach¶
Greedy with separate start and end times: 1. Separate start times and end times, sort both 2. Track rooms needed as we process times 3. When meeting starts, need room (increment) 4. When meeting ends, room freed (decrement) 5. Track maximum rooms needed simultaneously
This works because we only care about overlap count, not which specific meetings.
Solution¶
def min_meeting_rooms(intervals):
"""
Find minimum conference rooms needed.
Time Complexity: O(n log n) - sorting
Space Complexity: O(n) - separate arrays
"""
if not intervals:
return 0
# Separate start and end times
starts = sorted([i[0] for i in intervals])
ends = sorted([i[1] for i in intervals])
rooms = 0
max_rooms = 0
start_ptr = 0
end_ptr = 0
# Process all events
while start_ptr < len(intervals):
# Meeting starting
if starts[start_ptr] < ends[end_ptr]:
rooms += 1
max_rooms = max(max_rooms, rooms)
start_ptr += 1
# Meeting ending
else:
rooms -= 1
end_ptr += 1
return max_rooms
# Alternative: Using heap
import heapq
def min_meeting_rooms_heap(intervals):
"""Alternative using min heap."""
if not intervals:
return 0
# Sort by start time
intervals.sort(key=lambda x: x[0])
# Heap stores end times of ongoing meetings
heap = []
for start, end in intervals:
# Remove ended meetings
if heap and heap[0] <= start:
heapq.heappop(heap)
# Add current meeting
heapq.heappush(heap, end)
# Heap size is max concurrent meetings
return len(heap)
# Test
intervals = [[0,30],[5,10],[15,20]]
print(min_meeting_rooms(intervals)) # Output: 2
Step-by-Step Walkthrough¶
intervals = [[0,30],[5,10],[15,20]]
Sort separately:
starts = [0, 5, 15]
ends = [10, 20, 30]
Process timeline:
Time 0: meeting starts
rooms = 1, max = 1
start_ptr = 1, end_ptr = 0
Time 5: meeting starts (before any ends at 10)
rooms = 2, max = 2
start_ptr = 2, end_ptr = 0
Time 10: meeting ends (10 == ends[0])
Can't process yet (15 < 10 is false)
Time 15: meeting starts (after 10 ends)
15 >= 10, so meeting ended first
rooms = 1
end_ptr = 1
Then meeting starts:
rooms = 2, max = 2
start_ptr = 3
Done processing starts
Result: 2 rooms needed
Heap alternative:
Sort: [[0,30],[5,10],[15,20]]
Process [0,30]:
heap = [30]
size = 1
Process [5,10]:
5 < 30, can't remove
heap = [10, 30]
size = 2
Process [15,20]:
15 > 10, remove 10
heap = [30]
Add 20: heap = [20, 30]
size = 2
Result: 2
Problem 3: Partition Labels¶
Difficulty: Medium
LeetCode: #763
Problem Statement¶
Given a string, partition it into as many parts as possible so that each letter appears in at most one part. Return the sizes of the partitions.
Example:
Input: s = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation: "ababcbaca", "defegde", "hijhklij"
Approach¶
Greedy with last occurrence tracking: 1. Record last occurrence of each character 2. Track current partition's end (farthest last occurrence) 3. When reaching current partition end, create partition 4. Extend partition end if later character appears earlier
Greedy works because we always extend to include all occurrences.
Solution¶
def partition_labels(s):
"""
Partition string into maximum parts.
Time Complexity: O(n) - two passes
Space Complexity: O(1) - at most 26 characters
"""
# Record last occurrence of each character
last = {char: i for i, char in enumerate(s)}
partitions = []
start = 0
end = 0
for i, char in enumerate(s):
# Extend partition to include this character's last occurrence
end = max(end, last[char])
# Reached end of current partition
if i == end:
partitions.append(end - start + 1)
start = i + 1
return partitions
# Test
s = "ababcbacadefegdehijhklij"
print(partition_labels(s)) # Output: [9, 7, 8]
Step-by-Step Walkthrough¶
s = "ababcbacadefegdehijhklij"
Build last occurrence map:
a: 8, b: 5, c: 7, d: 14, e: 15, f: 11,
g: 13, h: 19, i: 22, j: 23, k: 20, l: 21
Process characters:
i=0, char='a':
end = max(0, last['a']=8) = 8
Not at end yet
i=1, char='b':
end = max(8, last['b']=5) = 8
Not at end yet
i=2, char='a':
end = max(8, last['a']=8) = 8
Not at end yet
...
i=5, char='b':
end = max(8, last['b']=5) = 8
Not at end yet
i=7, char='c':
end = max(8, last['c']=7) = 8
Not at end yet
i=8, char='a':
end = max(8, last['a']=8) = 8
i == end! Create partition
size = 8 - 0 + 1 = 9
partitions = [9]
start = 9
i=9, char='d':
end = max(8, last['d']=14) = 14
i=10, char='e':
end = max(14, last['e']=15) = 15
...
i=15, char='e':
end = max(15, last['e']=15) = 15
i == end! Create partition
size = 15 - 9 + 1 = 7
partitions = [9, 7]
start = 16
i=16, char='h':
end = max(15, last['h']=19) = 19
...
i=23, char='j':
end = max(23, last['j']=23) = 23
i == end! Create partition
size = 23 - 16 + 1 = 8
partitions = [9, 7, 8]
Result: [9, 7, 8]
Key Takeaways¶
- Proof Required: Greedy needs proof that local optimum leads to global
- Sorting Common: Many greedy problems start with sorting
- Simple Implementation: Usually simpler than DP alternatives
- Not Always Optimal: Greedy fails on some problems (0/1 knapsack)
- Fast Execution: Typically O(n log n) or better
Common Greedy Patterns¶
Interval Problems¶
- Activity selection
- Meeting rooms
- Non-overlapping intervals
- Merge intervals
Optimization Problems¶
- Huffman coding
- Fractional knapsack
- Job scheduling
- Minimum coins (specific denominations)
Sequence Problems¶
- Jump game
- Gas station
- Candy distribution
- Partition problems
Practice Tips¶
- Try greedy first for optimization problems
- Prove correctness with exchange argument or induction
- Sort data appropriately before making greedy choices
- Consider whether all cases are covered
- If greedy fails, switch to DP or backtracking
- Draw timeline or diagram to visualize choices
Template Pattern¶
def greedy_template(items):
"""General greedy algorithm template."""
# Step 1: Sort by some criterion
items.sort(key=lambda x: greedy_criterion(x))
result = initial_value
# Step 2: Make greedy choices
for item in items:
# Step 3: Check if choice is feasible
if is_feasible(item, result):
# Step 4: Update result with greedy choice
result = make_choice(item, result)
return result
# Interval scheduling template
def interval_template(intervals):
"""Template for interval problems."""
# Sort by end time (or start time depending on problem)
intervals.sort(key=lambda x: x[1])
count = 0
last_end = float('-inf')
for start, end in intervals:
if start >= last_end: # No overlap
count += 1
last_end = end
return count
Common Mistakes¶
- Assuming greedy works without proof
- Wrong sorting criterion
- Not considering edge cases
- Missing feasibility check
- Using greedy when DP is needed
- Incorrect greedy choice definition
- Not handling empty input