Greedy
Last updated: Jul 7, 2026Table of Contents
- Overview
- Key Properties
- Core Characteristics
- Greedy vs Other Approaches
- Templates & Algorithms
- Template Comparison Table
- Universal Greedy Template
- Template 1: Interval Scheduling — LC 435
- Template 2: Activity Selection with Heap — LC 621
- Template 3: Greedy Accumulation — LC 122
- Template 4: Jump Game Pattern — LC 55
- Template 5: String Reorganization — LC 767
- Template 6: Fractional Knapsack
- Problems by Pattern
- Interval Problems
- Activity Selection Problems
- Stock Trading Problems
- Jump Game Problems
- String Reorganization Problems
- Other Greedy Problems
- LC Examples
- 2-1) Jump Game (LC 55) — Greedy Max Reach
- 2-2) Jump Game II (LC 45) — Greedy Jump Window
- 2-3) Best Time to Buy and Sell Stock II (LC 122) — Accumulate Daily Gains
- 2-4) Gas Station (LC 134) — Greedy Start Reset
- 2-5) Reorganize String (LC 767) — Greedy Max-Heap Interleave
- 2-6) String Without AAA or BBB (LC 984) — Greedy Consecutive Counter
- 2-7) Task Scheduler (LC 621) — Greedy Idle Time Formula
- 2-8) Maximum Units on a Truck (LC 1710) — Sort by Unit Value
- 2-9) Max Non-Overlapping Subarrays With Sum = Target (LC 1546) — Greedy Prefix-Sum Reset
- Decision Framework
- Pattern Selection Strategy
- Greedy vs Dynamic Programming
- Missing Google Patterns
- Proof Template: Exchange Argument
- Minimum Spanning Tree (MST)
- Weighted Interval Scheduling — LC 1235
- Fractional vs 0/1 Knapsack
- Google Interview Tips for Greedy
- Summary & Quick Reference
- Complexity Quick Reference
- Sorting Criteria Guide
- Common Greedy Patterns
- Problem-Solving Steps
- Common Mistakes & Tips
- Proof Techniques
- Interview Tips
- Classic Greedy Problems
- Related Topics
Greedy Algorithms
Overview
Greedy algorithms make locally optimal choices at each step with the hope of finding a global optimum. They work by selecting the best available option at each decision point without reconsidering previous choices.
Key Properties
- Time Complexity: Usually O(n) or O(nlogn) with sorting
- Space Complexity: O(1) to O(n) depending on problem
- Core Idea: Make the locally optimal choice at each step
- When to Use: Problems with greedy choice property and optimal substructure
- Limitation: Doesn’t always yield globally optimal solution
Core Characteristics
- Greedy Choice Property: Local optimal leads to global optimal
- Optimal Substructure: Optimal solution contains optimal sub-solutions
- No Backtracking: Once a choice is made, it’s never reconsidered
- Proof Required: Must prove greedy approach gives optimal result
Greedy vs Other Approaches
- Greedy vs DP: Greedy is optimized DP when greedy choice works
- Greedy vs Brute Force: Much faster but may miss optimal
- Path: Brute Force → DP → Greedy (when applicable)
Templates & Algorithms
Template Comparison Table
| Template Type | Use Case | Sorting Key | When to Use |
|---|---|---|---|
| Interval | Non-overlapping selection | End time | Meeting rooms, activities |
| Priority Queue | Dynamic selection | Value/frequency | Task scheduling |
| Two Pointers | Pairing/matching | Various | Array manipulation |
| Accumulation | Running sum/product | None | Stock, gas station |
| Jump/Reach | Position tracking | None | Jump games |
Universal Greedy Template
def greedy_solution(items):
# Step 1: Sort or prepare data structure
items.sort(key=lambda x: x[criterion])
# Step 2: Initialize greedy choice tracking
result = initial_value
current_state = initial_state
# Step 3: Make greedy choices
for item in items:
if can_select(item, current_state):
result = update_result(result, item)
current_state = update_state(current_state, item)
return result
Template 1: Interval Scheduling — LC 435
def interval_scheduling(intervals):
"""Select maximum non-overlapping intervals"""
if not intervals:
return 0
# Sort by end time
intervals.sort(key=lambda x: x[1])
count = 1
end = intervals[0][1]
for i in range(1, len(intervals)):
if intervals[i][0] >= end:
count += 1
end = intervals[i][1]
return count
Template 2: Activity Selection with Heap — LC 621
import heapq
def activity_selection_heap(tasks):
"""Select activities using priority queue"""
# Count frequency or priority
freq = collections.Counter(tasks)
# Max heap (negate for min heap)
heap = [(-count, task) for task, count in freq.items()]
heapq.heapify(heap)
result = []
while heap:
count1, task1 = heapq.heappop(heap)
result.append(task1)
if heap:
count2, task2 = heapq.heappop(heap)
result.append(task2)
# Add back if still available
if count1 < -1:
heapq.heappush(heap, (count1 + 1, task1))
if count2 < -1:
heapq.heappush(heap, (count2 + 1, task2))
return result
Template 3: Greedy Accumulation — LC 122
def greedy_accumulation(prices):
"""Accumulate positive differences (stock trading)"""
profit = 0
for i in range(1, len(prices)):
# Greedy: take profit whenever possible
if prices[i] > prices[i-1]:
profit += prices[i] - prices[i-1]
return profit
Template 4: Jump Game Pattern — LC 55
def jump_game(nums):
"""Check if can reach end"""
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
if max_reach >= len(nums) - 1:
return True
return True
def jump_game_min_jumps(nums):
"""Minimum jumps to reach 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 jumps
Template 5: String Reorganization — LC 767
def reorganize_string(s):
"""Reorganize string so no adjacent chars are same"""
from collections import Counter
import heapq
# Count frequencies
count = Counter(s)
# Check if possible
max_count = max(count.values())
if max_count > (len(s) + 1) // 2:
return ""
# Max heap of frequencies
heap = [(-cnt, char) for char, cnt in count.items()]
heapq.heapify(heap)
result = []
prev_count, prev_char = 0, ''
while heap:
count, char = heapq.heappop(heap)
result.append(char)
# Add previous back to heap
if prev_count < 0:
heapq.heappush(heap, (prev_count, prev_char))
# Update previous
prev_count = count + 1
prev_char = char
return ''.join(result)
Template 6: Fractional Knapsack
def fractional_knapsack(items, capacity):
"""Greedy knapsack allowing fractions"""
# items = [(value, weight), ...]
# Sort by value/weight ratio
items.sort(key=lambda x: x[0]/x[1], reverse=True)
total_value = 0
remaining = capacity
for value, weight in items:
if weight <= remaining:
total_value += value
remaining -= weight
else:
# Take fraction
total_value += value * (remaining / weight)
break
return total_value
Problems by Pattern
Interval Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Non-overlapping Intervals | 435 | Sort by end | Medium |
| Minimum Arrows to Burst Balloons | 452 | Sort by end | Medium |
| Maximum Length of Pair Chain | 646 | Sort by end | Medium |
| Merge Intervals | 56 | Sort by start | Medium |
| Meeting Rooms II | 253 | Sort + heap | Medium |
| Interval List Intersections | 986 | Two pointers | Medium |
Activity Selection Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Task Scheduler | 621 | Frequency count | Medium |
| Maximum Events Attended | 1353 | Sort + heap | Medium |
| Course Schedule III | 630 | Sort + heap | Hard |
| IPO | 502 | Two heaps | Hard |
Stock Trading Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Buy Sell Stock II | 122 | Accumulate gains | Easy |
| Gas Station | 134 | Circular array | Medium |
| Best Time with Fee | 714 | State tracking | Medium |
| Container With Most Water | 11 | Two pointers | Medium |
Jump Game Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Jump Game | 55 | Track max reach | Medium |
| Jump Game II | 45 | Min jumps | Medium |
| Jump Game III | 1306 | BFS/DFS | Medium |
| Reach a Number | 754 | Math + greedy | Medium |
String Reorganization Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Reorganize String | 767 | Max heap | Medium |
| String Without AAA or BBB | 984 | Greedy + counter tracking | Medium |
| Rearrange K Distance Apart | 358 | Heap + queue | Hard |
| Task Scheduler | 621 | Frequency | Medium |
| Longest Happy String | 1405 | Heap greedy | Medium |
Other Greedy Problems
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Candy | 135 | Two pass | Hard |
| Assign Cookies | 455 | Two pointers | Easy |
| Maximum Units on Truck | 1710 | Sort by value | Easy |
| Boats to Save People | 881 | Two pointers | Medium |
| Minimum Cost to Connect Sticks | 1167 | Min heap | Medium |
| Max Non-Overlapping Subarrays Sum=Target | 1546 | Prefix sum + greedy reset | Medium |
LC Examples
2-1) Jump Game (LC 55) — Greedy Max Reach
Track farthest reachable index; if current index exceeds it, return false.
// LC 55 - Jump Game
// IDEA: Greedy — track max reachable index; fail if current index exceeds it
// time = O(N), space = O(1)
public boolean canJump(int[] nums) {
int maxReach = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxReach) return false;
maxReach = Math.max(maxReach, i + nums[i]);
}
return true;
}
# 055 Jump Game
# V0
class Solution(object):
def canJump(self, nums):
# edge case
if not nums:
return True
cur = 0
for i in range(len(nums)):
if cur < i:
return False
cur = max(cur, i + nums[i])
return True
2-2) Jump Game II (LC 45) — Greedy Jump Window
Expand the current jump window; when boundary is reached, take a jump and advance window.
// LC 45 - Jump Game II
// IDEA: Greedy — track current window end and farthest; jump when window end reached
// time = O(N), space = O(1)
public int jump(int[] nums) {
int jumps = 0, curEnd = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == curEnd) { jumps++; curEnd = farthest; }
}
return jumps;
}
# 045 Jump Game II
# V0
# IDEA : GREEDY
"""
Steps:
step 1) Initialize three integer variables: jumps to count the number of jumps, currentJumpEnd to mark the end of the range that we can jump to, and farthest to mark the farthest place that we can reach. Set each variable to zero
step 2) terate over nums. Note that we exclude the last element from our iteration because as soon as we reach the last element, we do not need to jump anymore.
- Update farthest to i + nums[i] if the latter is larger.
- If we reach currentJumpEnd, it means we finished the current jump, and can begin checking the next jump by setting currentJumpEnd = farthest.
step 3) return jumps
"""
class Solution:
def jump(self, nums: List[int]) -> int:
jumps = 0
current_jump_end = 0
farthest = 0
for i in range(len(nums) - 1):
# we continuously find the how far we can reach in the current jump
farthest = max(farthest, i + nums[i])
# if we have come to the end of the current jump,
# we need to make another jump
if i == current_jump_end:
jumps += 1
current_jump_end = farthest
return jumps
# V1
# IDEA : GREEDY
# https://leetcode.com/problems/jump-game-ii/discuss/1672485/Python-Greedy
class Solution:
def jump(self, nums: List[int]) -> int:
l = r = res = farthest = 0
while r < len(nums) - 1:
for idx in range(l, r+1):
farthest = max(farthest, idx + nums[idx])
l = r+1
r = farthest
res += 1
return res
2-3) Best Time to Buy and Sell Stock II (LC 122) — Accumulate Daily Gains
Accumulate every positive price difference — buy and sell every rising day.
// LC 122 - Best Time to Buy and Sell Stock II
// IDEA: Greedy — sum all positive consecutive differences
// time = O(N), space = O(1)
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++)
if (prices[i] > prices[i-1]) profit += prices[i] - prices[i-1];
return profit;
}
# 122 Best Time to Buy and Sell Stock II
class Solution:
def maxProfit(self, prices):
profit = 0
for i in range(0,len(prices)-1):
if prices[i+1] > prices[i]:
# have to sale last stock, then buy a new one
# and sum up the price difference into profit
profit += prices[i+1] - prices[i]
return profit
2-4) Gas Station (LC 134) — Greedy Start Reset
If tank goes negative, reset start to next station; valid solution exists iff total surplus ≥ 0.
// LC 134 - Gas Station
// IDEA: Greedy — reset start when cumulative surplus goes negative; check total >= 0
// time = O(N), space = O(1)
public int canCompleteCircuit(int[] gas, int[] cost) {
int total = 0, remain = 0, start = 0;
for (int i = 0; i < gas.length; i++) {
int diff = gas[i] - cost[i];
total += diff; remain += diff;
if (remain < 0) { start = i + 1; remain = 0; }
}
return total >= 0 ? start : -1;
}
# 134 Gas Station
# V0
# IDEA : GREEDY
# IDEA : if sum(gas) - sum(cost) > 0, => THERE MUST BE A SOLUTION
# IDEA : since it's circular (symmetry), we can maintain "total" (e.g. total += gas[i] - cost[i]) of (gas[i], cost[i]) for each index as their "current sum"
class Solution(object):
def canCompleteCircuit(self, gas, cost):
start = remain = total = 0
for i in range(len(gas)):
remain += gas[i] - cost[i]
total += gas[i] - cost[i]
if remain < 0:
remain = 0
start = i + 1
return -1 if total < 0 else start
2-5) Reorganize String (LC 767) — Greedy Max-Heap Interleave
Always pick the most frequent character that differs from the last placed character.
// LC 767 - Reorganize String
// IDEA: Max-heap by frequency; always pick top that differs from last placed char
// time = O(N log K), space = O(K) K = distinct chars
public String reorganizeString(String s) {
int[] freq = new int[26];
for (char c : s.toCharArray()) freq[c - 'a']++;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] - a[1]);
for (int i = 0; i < 26; i++) if (freq[i] > 0) pq.offer(new int[]{i, freq[i]});
StringBuilder sb = new StringBuilder();
int[] prev = null;
while (!pq.isEmpty()) {
int[] curr = pq.poll();
sb.append((char)('a' + curr[0]));
if (prev != null) pq.offer(prev);
curr[1]--;
prev = curr[1] > 0 ? curr : null;
}
return sb.length() == s.length() ? sb.toString() : "";
}
# LC 767. Reorganize String
# V0
# IDEA : GREEDY + COUNTER
# IDEA :
# step 1) order exists count (big -> small)
# step 2) select the element which is "most remaining" and DIFFERENT from last ans element and append such element to the end of ans
# step 3) if can't find such element, return ""
class Solution(object):
def reorganizeString(self, S):
cnt = collections.Counter(S)
# Be aware of it : ans = "#" -> not to have error in ans[-1] when first loop
ans = '#'
while cnt:
stop = True
for v, c in cnt.most_common():
"""
NOTE !!! trick here
1) we only compare last element in ans and current key (v), if they are different, then append
2) we break at the end of each for loop -> MAKE SURE two adjacent characters are not the same.
3) we use a flag "stop", to decide whether should stop while loop or not
"""
if v != ans[-1]:
stop = False
ans += v
cnt[v] -= 1
if not cnt[v]:
del cnt[v]
"""
NOTE !!!
-> we BREAK right after each op, since we want to get next NEW most common element from "updated" cnt.most_common()
"""
break
# Be aware of it : if there is no valid "v", then the while loop will break automatically at this condition (stop = True)
if stop:
break
return ans[1:] if len(ans[1:]) == len(S) else ''
2-6) String Without AAA or BBB (LC 984) — Greedy Consecutive Counter
Force a switch when 2 consecutive same chars; otherwise always write the higher-count char.
// LC 984 - String Without AAA or BBB
// IDEA: Greedy — write higher-count char unless 2 consecutive, then must switch
// time = O(A+B), space = O(1)
public String strWithout3a3b(int a, int b) {
StringBuilder res = new StringBuilder();
int continueA = 0;
int continueB = 0;
while (a > 0 || b > 0) {
boolean writeA = false;
// Priority 1: Must switch if 2 consecutive
if (continueB == 2) {
writeA = true;
} else if (continueA == 2) {
writeA = false;
} else {
// Priority 2: Greedy - write the one with more remaining
writeA = a >= b;
}
if (writeA) {
res.append("a");
a--;
continueA++;
continueB = 0; // Reset other counter
} else {
res.append("b");
b--;
continueB++;
continueA = 0; // Reset other counter
}
}
return res.toString();
}
// V1: Using last 2 characters check (Editorial)
public String strWithout3a3b_v1(int A, int B) {
StringBuilder ans = new StringBuilder();
while (A > 0 || B > 0) {
boolean writeA = false;
int L = ans.length();
// Check last 2 chars
if (L >= 2 && ans.charAt(L-1) == ans.charAt(L-2)) {
if (ans.charAt(L-1) == 'b') writeA = true;
} else {
if (A >= B) writeA = true;
}
if (writeA) { A--; ans.append('a'); }
else { B--; ans.append('b'); }
}
return ans.toString();
}
// V2: PQ approach (similar to Reorganize String)
// Max heap: always pick char with highest remaining count
// If blocked by consecutive constraint, pick second highest
Similar Problems:
- LC 767: Reorganize String (no 2 adjacent same)
- LC 1405: Longest Happy String (max a, b, c with no 3 consecutive)
- LC 358: Rearrange String K Distance Apart (k distance constraint)
2-7) Task Scheduler (LC 621) — Greedy Idle Time Formula
Min time = max((maxFreq−1)*(n+1) + countOfMaxFreq, totalTasks).
// LC 621 - Task Scheduler
// IDEA: Greedy formula — (maxFreq-1)*(n+1) + #tasks_with_maxFreq; also can't be less than total
// time = O(N), space = O(1)
public int leastInterval(char[] tasks, int n) {
int[] freq = new int[26];
for (char t : tasks) freq[t - 'A']++;
int maxFreq = 0;
for (int f : freq) maxFreq = Math.max(maxFreq, f);
int countMax = 0;
for (int f : freq) if (f == maxFreq) countMax++;
return Math.max(tasks.length, (maxFreq - 1) * (n + 1) + countMax);
}
# LC 621. Task Scheduler
# V0
# pattern :
# =============================================================================
# -> task_time = (max_mission_count - 1) * (n + 1) + (number_of_max_mission)
# =============================================================================
#
# -> Example 1) :
# -> AAAABBBBCCD, n=3
# => THE EXPECTED tuned missions is like : ABXXABXXABXXAB
# -> (4 - 1) * (3 + 1) + 2 = 14
# -> 4 is the "how many missions the max mission has" (AAAA or BBBB)
# -> 3 is n
# -> 2 is "how many mission have max mission count" -> A and B. so it's 2
# -> in sum,
# -> (4 - 1) * (3 + 1) is for ABXXABXXABXX
# -> and 2 is for AB
#
# -> Example 2) :
# -> AAABBB, n = 2
# -> THE EXPECTED tuned missions is like : ABXABXAB
# -> (3 - 1) * (2 + 1) + (2) = 8
class Solution(object):
def leastInterval(self, tasks, n):
count = collections.Counter(tasks)
most = count.most_common()[0][1]
num_most = len([i for i, v in count.items() if v == most])
"""
example 1 : tasks = ["A","A","A","B","B","B"], n = 2
-> we can split tasks as : A -> B -> idle -> A -> B -> idle -> A -> B
-> 1) so there are 3-1 group. e.g. (A -> B -> idle), (A -> B -> idle)
and each group has (n+1) elements. e.g. (A -> B -> idle)
-> 2) and the remain element is num_most. e.g. (A, B)
-> 3) so total cnt = (3-1) * (2+1) + 2 = 8
example 2 : tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
-> we can split tasks as A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> idle -> idle -> A -> idle -> idle -> A
-> 1) so there are 6-1 group. e.g. (A -> B -> C), (A -> D -> E), (A -> F -> G), (A -> idle -> idle), (A -> idle -> idle)
and each group has (n+1) elements. e.g. (A,B,C) .... (as above)
-> 2) and the remain element is num_most. e.g. (A)
-> 3) so total cnt = (6-1)*(2+1) + 1 = 16
"""
time = (most - 1) * (n + 1) + num_most
return max(time, len(tasks)) # when idle slots go negative, just run all tasks sequentially
2-8) Maximum Units on a Truck (LC 1710) — Sort by Unit Value
Sort box types by units per box descending; greedily fill truck with highest-value boxes first.
// LC 1710 - Maximum Units on a Truck
// IDEA: Sort by units descending; greedily load boxes until truck is full
// time = O(N log N), space = O(1)
public int maximumUnits(int[][] boxTypes, int truckSize) {
Arrays.sort(boxTypes, (a, b) -> b[1] - a[1]);
int total = 0;
for (int[] box : boxTypes) {
int take = Math.min(box[0], truckSize);
total += take * box[1];
truckSize -= take;
if (truckSize == 0) break;
}
return total;
}
# LC 1710. Maximum Units on a Truck
# V0
# IDEA : GREEDY + sorting
class Solution(object):
def maximumUnits(self, boxTypes, truckSize):
# boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi]:
# edge case
if not boxTypes or not truckSize:
return 0
"""
NOTE : we sort on sort(key=lambda x : -x[1])
-> if unit is bigger, we can get bigger aggregated result (n * unit)
"""
boxTypes.sort(key=lambda x : -x[1])
res = 0
for n, unit in boxTypes:
# case 1 : truckSize == 0, break for loop and return ans
if truckSize == 0:
break
# case 2 : truckSize < n, we CAN'T add all n to truck, but CAN only add (truckSize * unit) amount to truck
elif truckSize < n:
res += (truckSize * unit)
truckSize = 0
break
# case 3 : normal case, it's OK to put all (n * unit) to truck once
else:
res += (n * unit)
truckSize -= n
return res
# V1
# IDEA : GREEDY
# https://leetcode.com/problems/maximum-units-on-a-truck/discuss/1045318/Python-solution
class Solution(object):
def maximumUnits(self, boxTypes, truckSize):
boxTypes.sort(key = lambda x: -x[1])
n = len(boxTypes)
result = 0
i = 0
while truckSize >= boxTypes[i][0]:
result += boxTypes[i][1] * boxTypes[i][0]
truckSize -= boxTypes[i][0]
i += 1
if i == n:
return result
result += truckSize * boxTypes[i][1]
return result
2-9) Max Non-Overlapping Subarrays With Sum = Target (LC 1546) — Greedy Prefix-Sum Reset
The moment a valid subarray is found, lock it in and clear all history — earliest-ending choice leaves the most room for future subarrays.
The Greedy Clear Idea
# Check if there is a matching complement prefix sum
if (running_prefix - target) in seen_prefixes:
cnt += 1
# GREEDY RESET: Clear everything to prevent overlaps
seen_prefixes.clear()
running_prefix = 0
Why clear / reset works (the greedy argument):
- We use prefix sums: a subarray
nums[i+1..j]sums totargetiffprefix[j] - prefix[i] == target, i.e.prefix[j] - targetwas a prefix sum we’ve seen before. - The instant we detect such a match, we’ve found a valid subarray ending as early as possible at the current index
j. - Greedy choice property: taking the earliest-ending valid subarray is never worse than waiting for a later one. An earlier finish frees up the maximum number of remaining elements for future subarrays — it can only help, never hurt, the total count. (This is the same “sort by end time” intuition as interval scheduling — here the end time is discovered on the fly.)
- To guarantee the next subarray does not overlap the one we just took, everything before/at index
jmust become invisible. Clearingseen_prefixesand resettingrunning_prefix = 0makes indexjthe new “virtual start” — future matches can only use complements formed afterj.
⚠️ Order matters: you must re-add the base
0before continuing. In the V1-1 style it’sseen = set([0])on reset; in the V1-2 styleseen_prefixes.clear()is immediately followed (after theif) byseen_prefixes.add(running_prefix)whererunning_prefixis now0— so0re-enters the set. Without a0in the fresh set, the very next subarray starting right afterjcould never be detected.
Visualization
nums = [-1, 3, 5, 1, 4, 2, -9], target = 6
Legend: prefix = running prefix sum
complement = prefix - target (what we look for in `seen`)
seen = prefix sums since the LAST reset
┌─────┬────────┬────────────┬────────┬─────┬──────────────────┐
│ num │ prefix │ complement │ found? │ cnt │ seen (this seg) │
├─────┼────────┼────────────┼────────┼─────┼──────────────────┤
│ init│ 0 │ - │ - │ 0 │ {0} │
│ -1 │ -1 │ -7 │ No │ 0 │ {0,-1} │
│ 3 │ 2 │ -4 │ No │ 0 │ {0,-1,2} │
│ 5 │ 7 │ 1 │ No │ 0 │ {0,-1,2,7} │
│ 1 │ 8 │ 2 │ YES │ 1 │ RESET → {0} │ ← subarray [5,1] sums to 6
│ 4 │ 4 │ -2 │ No │ 1 │ {0,4} │
│ 2 │ 6 │ 0 │ YES │ 2 │ RESET → {0} │ ← subarray [4,2] sums to 6
│ -9 │ -9 │ -15 │ No │ 2 │ {0,-9} │
└─────┴────────┴────────────┴────────┴─────┴──────────────────┘
Answer: cnt = 2 (subarrays [5,1] and [4,2] are non-overlapping)
Why NOT wait for a bigger subarray?
Full array [3,5,1,4,2,-9] also sums to 6, but choosing it would
"swallow" indices that could otherwise host BOTH [5,1] and [4,2]:
[-1, 3, 5, 1, 4, 2, -9]
└──6──┘ ← take EARLY → 1 subarray, rest still free
└──6──┘ ← then take another → total = 2 ✅
[-1, 3, 5, 1, 4, 2, -9]
└──────── 6 ────────┘ ← greedy-late swallows everything → total = 1 ❌
// java
// LC 1546 - Max Non-Overlapping Subarrays With Sum = Target
// IDEA: Greedy + prefix sum + hashset; on match, count and RESET history
// time = O(N), space = O(N)
public int maxNonOverlapping(int[] nums, int target) {
Set<Integer> seen = new HashSet<>();
seen.add(0); // base prefix
int running = 0, cnt = 0;
for (int num : nums) {
running += num;
if (seen.contains(running - target)) {
cnt++;
seen.clear(); // GREEDY RESET: prevent overlap
running = 0;
}
seen.add(running); // re-adds 0 right after a reset
}
return cnt;
}
# python
# LC 1546 - Max Non-Overlapping Subarrays With Sum = Target
# IDEA: Greedy + prefix sum + hashset; on match, count and RESET history
# time = O(N), space = O(N)
class Solution(object):
def maxNonOverlapping(self, nums, target):
seen_prefixes = set([0]) # base prefix
running_prefix = 0
cnt = 0
for num in nums:
running_prefix += num
if (running_prefix - target) in seen_prefixes:
cnt += 1
# GREEDY RESET: clear everything to prevent overlaps
seen_prefixes.clear()
running_prefix = 0
seen_prefixes.add(running_prefix) # re-adds 0 right after a reset
return cnt
Similar Problems:
- LC 560 Subarray Sum Equals K (count ALL subarrays — no reset, keep full history)
- LC 974 Subarray Sums Divisible by K (prefix-sum + hashmap on remainders)
- LC 134 Gas Station (greedy start reset when running total goes negative)
- LC 435 Non-overlapping Intervals (same earliest-end greedy, but intervals given upfront)
Decision Framework
Pattern Selection Strategy
Greedy Algorithm Selection Flowchart:
1. Can the problem be solved greedily?
├── Does local optimal lead to global optimal? → YES → Use Greedy
├── Can you prove greedy correctness? → YES → Use Greedy
└── NO to both → Use DP or other approach
2. What type of greedy pattern?
├── Selection from sorted items → Interval/Activity Selection
├── Maximize/minimize at each step → Accumulation Pattern
├── Dynamic selection → Priority Queue/Heap
├── Position/reach tracking → Jump Game Pattern
└── Pairing/matching → Two Pointers
3. How to make greedy choice?
├── Sort by what criterion?
│ ├── End time → Interval scheduling
│ ├── Start time → Merge intervals
│ ├── Value/weight ratio → Knapsack
│ └── Custom criterion → Problem specific
└── No sorting needed → Direct iteration
4. Common greedy strategies:
├── Always take the best available
├── Never make a choice that blocks future options
├── Minimize waste/maximize efficiency
└── Balance resources evenly
Greedy vs Dynamic Programming
| Criterion | Use Greedy | Use DP | Example |
|---|---|---|---|
| Greedy choice property | ✅ | ❌ | Activity selection |
| Need all sub-solutions | ❌ | ✅ | 0/1 Knapsack |
| Can prove optimality | ✅ | - | Huffman coding |
| Overlapping subproblems | ❌ | ✅ | Fibonacci |
| Simple selection rule | ✅ | ❌ | Fractional knapsack |
Missing Google Patterns
Proof Template: Exchange Argument
To verify a greedy choice, show that swapping the greedy pick with any other choice does not improve the result.
1. Assume optimal solution OPT differs from greedy solution G at some step.
2. Show you can swap OPT's choice at that step with G's choice without making things worse.
3. Repeat until OPT == G → greedy is optimal.
Common exchange argument problems: LC 435 (Non-overlapping Intervals), LC 452 (Burst Balloons), Job Scheduling.
Minimum Spanning Tree (MST)
Kruskal’s (sort edges, union-find):
def kruskal(n, edges):
edges.sort(key=lambda x: x[2]) # sort by weight
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
a, b = find(a), find(b)
if a == b: return False
parent[a] = b
return True
mst_cost = 0
for u, v, w in edges:
if union(u, v):
mst_cost += w
return mst_cost
Prim’s (priority queue, dense graphs):
import heapq
from collections import defaultdict
def prim(n, edges):
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((w, v))
graph[v].append((w, u))
visited = set()
heap = [(0, 0)] # (cost, node)
total = 0
while heap and len(visited) < n:
cost, node = heapq.heappop(heap)
if node in visited: continue
visited.add(node)
total += cost
for w, nei in graph[node]:
if nei not in visited:
heapq.heappush(heap, (w, nei))
return total
| Algorithm | Time | Best For |
|---|---|---|
| Kruskal | O(E log E) | Sparse graphs |
| Prim (heap) | O(E log V) | Dense graphs |
Weighted Interval Scheduling — LC 1235
When intervals have weights/profits, greedy alone fails — use DP + binary search.
# LC 1235 Maximum Profit in Job Scheduling
import bisect
def jobScheduling(startTime, endTime, profit):
jobs = sorted(zip(startTime, endTime, profit), key=lambda x: x[1])
dp = [(0, 0)] # (end_time, max_profit)
for s, e, p in jobs:
# Find last job that ends <= s
i = bisect.bisect_right(dp, (s, float('inf'))) - 1
new_profit = dp[i][1] + p
if new_profit > dp[-1][1]:
dp.append((e, new_profit))
return dp[-1][1]
Fractional vs 0/1 Knapsack
| Property | Fractional | 0/1 |
|---|---|---|
| Can split items | Yes | No |
| Algorithm | Greedy (sort by value/weight) | DP |
| Time | O(n log n) | O(nW) |
| Greedy works? | Yes | No |
Why greedy fails for 0/1: Counter-example: items [(value=6, w=4), (value=5, w=3), (value=5, w=3)], capacity=6. Greedy picks highest ratio (item1, ratio=1.5) → only gets 6. DP picks item2+item3 → gets 10.
Google Interview Tips for Greedy
| Signal | Pattern |
|---|---|
| “minimum cost to connect” | MST (Kruskal/Prim) |
| “maximize non-overlapping intervals” | Sort by end time |
| “schedule tasks with cooldown” | Math formula or max-heap |
| “fractional items” | Sort by value/weight ratio |
| “prove this greedy works” | Exchange argument |
| “greedy gives wrong answer here” | Switch to DP |
Summary & Quick Reference
Complexity Quick Reference
| Pattern | Time Complexity | Space Complexity | Bottleneck |
|---|---|---|---|
| Interval scheduling | O(nlogn) | O(1) | Sorting |
| Heap-based selection | O(nlogn) | O(n) | Heap operations |
| Two pointers | O(n) or O(nlogn) | O(1) | Sorting if needed |
| Direct accumulation | O(n) | O(1) | Single pass |
| Jump game | O(n) | O(1) | Single pass |
Sorting Criteria Guide
# Interval problems
intervals.sort(key=lambda x: x[1]) # By end time
intervals.sort(key=lambda x: x[0]) # By start time
# Value optimization
items.sort(key=lambda x: x.value/x.weight, reverse=True) # By ratio
# Custom priority
tasks.sort(key=lambda x: (x.deadline, -x.profit)) # Multi-criteria
Common Greedy Patterns
Exchange Argument
# Prove: Swapping any two elements won't improve result
def exchange_argument_proof(arr):
# If swapping arr[i] and arr[j] doesn't improve,
# then current order is optimal
pass
Greedy Stays Ahead
# Prove: Greedy solution is at least as good at each step
def stays_ahead_proof(greedy, other):
# Show: greedy[i] >= other[i] for all i
pass
Matroid Theory
# System has matroid structure if:
# 1. Hereditary: Subset of feasible is feasible
# 2. Exchange: Can always extend smaller feasible set
Problem-Solving Steps
- Identify greedy potential: Look for optimal substructure
- Define greedy choice: What to select at each step
- Prove correctness: Exchange argument or stays ahead
- Implement efficiently: Often requires sorting
- Handle edge cases: Empty input, single element
- Verify with examples: Test greedy choices
Common Mistakes & Tips
🚫 Common Mistakes:
- Assuming greedy works without proof
- Wrong sorting criterion
- Not considering all edge cases
- Forgetting to handle ties
- Missing global constraint checks
✅ Best Practices:
- Always verify greedy property first
- Start with small examples
- Consider counter-examples
- Use heap for dynamic selection
- Test with edge cases
Proof Techniques
Exchange Argument Example
# Prove interval scheduling is optimal
# If we swap any interval in greedy solution with another,
# we either get same or fewer intervals
Greedy Stays Ahead Example
# Prove jump game solution is minimal
# At each position, greedy reaches at least as far
Interview Tips
- Recognize patterns: Look for sorting or selection hints
- Start with examples: Work through small cases
- State assumptions: Clarify if greedy is applicable
- Prove if asked: Use exchange or stays ahead
- Code cleanly: Greedy code is usually simple
- Optimize: Consider using heap for better complexity
Classic Greedy Problems
- Activity Selection: Choose maximum non-overlapping
- Huffman Coding: Build optimal prefix codes
- Kruskal’s MST: Select minimum weight edges
- Dijkstra’s: Select minimum distance vertex
- Fractional Knapsack: Take most valuable ratio
Related Topics
- Dynamic Programming: When greedy doesn’t work
- Binary Search: For optimization problems
- Heap/Priority Queue: For dynamic selection
- Sorting: Often prerequisite for greedy
- Graph Algorithms: Many use greedy (MST, shortest path)