Greedy
Table of Contents
- # Overview
- # Key Properties
- # Core Characteristics
- # Greedy vs Other Approaches
- # Problem Categories
- # Category 1: Interval Scheduling
- # Category 2: Activity Selection
- # Category 3: Huffman Coding
- # Category 4: Stock Trading
- # Category 5: Jump Game
- # Category 6: String Reorganization
- # Templates & Algorithms
- # Template Comparison Table
- # Universal Greedy Template
- # Template 1: Interval Scheduling
- # Template 2: Activity Selection with Heap
- # Template 3: Greedy Accumulation
- # Template 4: Jump Game Pattern
- # Template 5: String Reorganization
- # 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-2) Jump Game
- # 2-2β) Jump Game II
- # 2-3) Best Time to Buy and Sell Stock II
- # 2-4) Gas Station
- # 2-6) Reorganize String
- # 2-7) Task Scheduler
- # 2-8) Maximum Units on a Truck
- # Decision Framework
- # Pattern Selection Strategy
- # Greedy vs Dynamic Programming
- # Greedy vs BFS/DFS in Graph Problems
- # When to Choose: Decision Framework
- # Complexity Comparison
- # Problem Size Considerations
- # Decision Checkpoints
- # Common Graph Problem Patterns
- # LeetCode Examples with Size Analysis
- # Interview Strategy Guide
- # Practical Performance Benchmarks
- # Summary: Algorithm Selection Matrix
- # 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)
# Problem Categories
# Category 1: Interval Scheduling
- Description: Select maximum non-overlapping intervals
- Examples: LC 435 (Non-overlapping), LC 452 (Burst Balloons), LC 646 (Pair Chain)
- Pattern: Sort by end time, select earliest ending
# Category 2: Activity Selection
- Description: Maximize number of activities or minimize resources
- Examples: LC 621 (Task Scheduler), LC 1353 (Maximum Events)
- Pattern: Priority queue or sorting with selection
# Category 3: Huffman Coding
- Description: Build optimal prefix codes
- Examples: LC 1167 (Min Cost to Connect Sticks)
- Pattern: Repeatedly merge smallest elements
# Category 4: Stock Trading
- Description: Maximize profit from transactions
- Examples: LC 122 (Buy Sell Stock II), LC 134 (Gas Station)
- Pattern: Accumulate positive differences
# Category 5: Jump Game
- Description: Reach target with minimum steps
- Examples: LC 55 (Jump Game), LC 45 (Jump Game II)
- Pattern: Track maximum reachable position
# Category 6: String Reorganization
- Description: Rearrange strings with constraints
- Examples: LC 767 (Reorganize String), LC 358 (Rearrange K Distance)
- Pattern: Use heap to select most frequent available
# 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
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
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
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
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
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 |
| 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 |
# LC Examples
# 2-2) Jump Game
# 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
# 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
# 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
# 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-6) Reorganize String
# 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-7) Task Scheduler
# 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)) # be aware of it
# 2-8) Maximum Units on a Truck
# 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
# 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 |
# Greedy vs BFS/DFS in Graph Problems
# When to Choose: Decision Framework
Graph Problem Algorithm Selection:
1. Problem Goal Analysis
βββ Find shortest path (unweighted) β BFS
βββ Find shortest path (weighted, all positive) β Dijkstra (Greedy)
βββ Find shortest path (negative weights) β Bellman-Ford (DP)
βββ Explore all paths/solutions β DFS
βββ Minimum spanning tree β Kruskal/Prim (Greedy)
βββ Topological sort β DFS or BFS (Kahn's)
βββ Connectivity/reachability β DFS or BFS
2. Problem Constraints Check
βββ Graph size (|V| + |E|) > 10^5 β Favor Greedy if applicable
βββ Dense graph (E β VΒ²) β Consider space complexity
βββ Sparse graph (E β V) β BFS/DFS usually fine
βββ Special structure (DAG, tree) β Enables certain optimizations
3. Correctness Requirements
βββ Need to explore all possibilities β DFS/BFS required
βββ Greedy choice property proven β Greedy is optimal
βββ Optimal substructure exists β Consider DP or Greedy
βββ No optimal substructure β BFS/DFS for complete search
# Complexity Comparison
| Algorithm | Time Complexity | Space Complexity | Best Use Case | Graph Size Limit |
|---|---|---|---|---|
| BFS | O(V + E) | O(V) | Shortest path (unweighted), level-order | < 10^6 nodes |
| DFS | O(V + E) | O(V) | Path finding, cycle detection, topological sort | < 10^6 nodes |
| Dijkstra (Greedy) | O((V + E)logV) | O(V) | Shortest path (weighted, positive) | < 10^5 nodes |
| Kruskal (Greedy) | O(ElogE) | O(V + E) | Minimum spanning tree | < 10^5 edges |
| Prim (Greedy) | O(ElogV) | O(V + E) | MST for dense graphs | < 10^5 nodes |
# Problem Size Considerations
# Critical Thresholds
Small Scale (|V| + |E| < 10^3)
- β Any algorithm works
- Choose based on code simplicity
- Performance differences negligible
- Focus on correctness
Medium Scale (10^3 β€ |V| + |E| β€ 10^5)
- β οΈ Algorithm choice matters
- BFS/DFS: Usually acceptable
- Greedy: Preferred if applicable (faster constants)
- Consider optimization for dense graphs
Large Scale (|V| + |E| > 10^5)
- π¨ Critical to choose correctly
- BFS/DFS: May timeout if O(VΒ²) or O(VE)
- Greedy: Strongly preferred if problem allows
- Space complexity becomes critical
- Consider:
- Memory limits (often 256MB in contests)
- Time limits (1-2 seconds typical)
- Constant factors in complexity
# When Graph Size Forces Greedy Choice
| Scenario | Size Threshold | Why Greedy Preferred | Example Problem |
|---|---|---|---|
| Dense graph shortest path | V > 10^4, E β VΒ² | BFS O(VΒ²) too slow | LC 743 Network Delay Time |
| MST in large graph | E > 10^5 | Must avoid exploring all combinations | LC 1584 Min Cost Connect Points |
| Large sparse graph | V > 10^5, E β V | Greedy O(ElogE) vs BFS O(VE) | LC 1631 Path With Min Effort |
| Real-time path finding | V > 10^5 | Need fast response | Navigation systems |
# Decision Checkpoints
# Checkpoint 1: Can Greedy Work? (Mandatory Checks)
β Use Greedy if ALL are true:
-
Greedy choice property exists
- Local optimal choice leads to global optimal
- Can prove via exchange argument or contradiction
-
Problem has special structure
- Shortest path with non-negative weights (Dijkstra)
- Minimum spanning tree (Kruskal/Prim)
- Interval scheduling patterns
- Optimal substructure with greedy choice
-
Performance requirement
- Large input size (> 10^5)
- Need O(ElogV) or better complexity
β Cannot use Greedy if ANY are true:
- Need to find ALL paths/solutions
- Negative edge weights exist
- Must backtrack or reconsider choices
- No proven greedy strategy exists
# Checkpoint 2: Which BFS/DFS? (If Greedy Doesnβt Apply)
Use BFS when:
- β Need shortest path in unweighted graph
- β Want level-by-level exploration
- β Need minimum steps/moves
- β Problem asks for βnearestβ or βminimum depthβ
- π Complexity: O(V + E), Space: O(V) queue
Use DFS when:
- β Need to explore all paths
- β Checking connectivity or cycles
- β Topological sorting
- β Path reconstruction required
- β Smaller space complexity acceptable (recursion stack)
- π Complexity: O(V + E), Space: O(V) call stack
# Checkpoint 3: Performance Analysis
# Decision tree based on constraints
def choose_algorithm(V, E, has_negative_weights, need_all_paths):
graph_size = V + E
if need_all_paths:
return "DFS" if V < 10000 else "Not feasible"
if has_negative_weights:
return "Bellman-Ford (DP)" if graph_size < 10^4 else "Not feasible"
# Shortest path problems
if is_weighted_graph:
if graph_size > 10^5:
return "Dijkstra (Greedy) - Required"
return "Dijkstra (Greedy) - Preferred"
else: # unweighted
if graph_size > 10^6:
return "Optimize or Not feasible"
return "BFS"
# MST problems
if is_mst_problem:
if E > 10^5:
return "Kruskal (Greedy) - Required"
return "Kruskal/Prim (Greedy)"
# Common Graph Problem Patterns
# Pattern 1: Shortest Path Problems
| Problem Type | Size < 10^3 | Size 10^3-10^5 | Size > 10^5 | Algorithm |
|---|---|---|---|---|
| Unweighted | BFS | BFS | BFS | O(V+E) |
| Weighted (positive) | Dijkstra | Dijkstra | Dijkstra (required) | O((V+E)logV) |
| Weighted (negative) | Bellman-Ford | Bellman-Ford | Not feasible* | O(VE) |
| All pairs | Floyd-Warshall | BFS from each | Not feasible | O(VΒ³) |
*For size > 10^5 with negative weights, problem is typically not solvable in reasonable time
# Pattern 2: MST Problems
# Kruskal's Algorithm (Greedy) - REQUIRED for large graphs
def kruskal_mst(edges, n):
"""
When to use: ANY MST problem, especially when E > 10^4
Time: O(E log E)
Space: O(V + E)
"""
edges.sort(key=lambda x: x[2]) # Sort by weight - GREEDY CHOICE
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
mst_cost = 0
for u, v, weight in edges:
pu, pv = find(u), find(v)
if pu != pv: # Greedy: take smallest edge that doesn't form cycle
parent[pu] = pv
mst_cost += weight
return mst_cost
# Why not BFS/DFS for MST?
# - Would need to explore all possible spanning trees: O(V^V) - INFEASIBLE
# - Greedy approach proven optimal (cut property, cycle property)
# Pattern 3: Reachability/Connectivity
| Problem Type | Best Algorithm | When Size > 10^5 | Reasoning |
|---|---|---|---|
| Can reach target? | DFS or BFS | Use BFS (iterative) | DFS recursion may overflow |
| Connected components | DFS or Union-Find | Union-Find (Greedy) | O(EβΊ(V)) vs O(V+E) |
| Cycle detection | DFS | DFS with iterative deepening | Need backtracking info |
| Bipartite check | BFS or DFS | BFS preferred | Better cache locality |
# LeetCode Examples with Size Analysis
# Greedy Required (Large Size)
LC 743: Network Delay Time
- Constraint: N β€ 100, K β€ 6000 (edges)
- Why Greedy: Weighted shortest path with positive weights
- Algorithm: Dijkstra (Greedy)
- BFS/DFS fails: Unweighted BFS gives wrong answer, DFS explores all paths (exponential)
def networkDelayTime(times, n, k):
# Dijkstra - Greedy choice: always extend shortest known distance
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
dist = {i: float('inf') for i in range(1, n + 1)}
dist[k] = 0
heap = [(0, k)] # (distance, node)
while heap:
d, node = heapq.heappop(heap)
if d > dist[node]:
continue
for nei, weight in graph[node]:
new_dist = d + weight
if new_dist < dist[nei]: # Greedy choice
dist[nei] = new_dist
heapq.heappush(heap, (new_dist, nei))
return max(dist.values()) if max(dist.values()) < float('inf') else -1
LC 1584: Min Cost to Connect All Points
- Constraint: N β€ 1000 points (up to 1000Β² edges)
- Why Greedy: MST problem, E could be ~10^6
- Algorithm: Kruskal or Prim (Greedy)
- BFS/DFS fails: No clear BFS/DFS strategy exists for MST
LC 1631: Path With Minimum Effort
- Constraint: rows * cols β€ 10^6
- Why Greedy: Need minimum maximum difference (variant of shortest path)
- Algorithm: Dijkstra with modified distance
- BFS fails: Need to consider edge weights
# BFS/DFS Optimal (Small to Medium Size)
LC 200: Number of Islands
- Constraint: m * n β€ 10^4
- Why BFS/DFS: Simple connectivity check
- Algorithm: DFS or BFS
- Greedy doesnβt apply: No optimization problem, just exploration
LC 207: Course Schedule
- Constraint: numCourses β€ 2000, prerequisites β€ 5000
- Why BFS/DFS: Cycle detection in directed graph
- Algorithm: DFS (topological sort) or BFS (Kahnβs)
- Greedy doesnβt apply: Must check all dependencies
LC 994: Rotting Oranges
- Constraint: grid size β€ 100
- Why BFS: Multi-source shortest path (unweighted)
- Algorithm: BFS
- Greedy not needed: Small size, unweighted
# Both Can Work (Trade-offs)
LC 785: Is Graph Bipartite?
- Size: Small (graph.length β€ 100)
- BFS approach: Color nodes level by level - O(V + E)
- Union-Find (Greedy): Group same-colored nodes - O(EβΊ(V))
- Choice: BFS simpler to code, Union-Find faster for dense graphs
# Interview Strategy Guide
# Quick Decision Algorithm
Given a graph problem in interview:
Step 1: Identify problem type (30 seconds)
βββ Shortest path? β Check weights
βββ MST? β Greedy (Kruskal/Prim)
βββ All paths? β DFS
βββ Minimum steps? β BFS
βββ Connectivity? β DFS/BFS or Union-Find
Step 2: Check size constraints (10 seconds)
βββ Size > 10^5? β Must use optimal algorithm
βββ Dense graph? β Consider space complexity
βββ Small size? β Focus on correctness
Step 3: Verify algorithm choice (20 seconds)
βββ Can I prove greedy works? β Use Greedy
βββ Need to explore all? β Use DFS
βββ Need shortest unweighted? β Use BFS
βββ Complex requirements? β Discuss trade-offs
Step 4: Code and optimize (remaining time)
# Common Mistakes to Avoid
π« Using BFS for weighted shortest path
- Problem: BFS assumes all edges equal weight
- Fix: Use Dijkstra (Greedy) for weighted graphs
π« Using Dijkstra with negative weights
- Problem: Greedy assumption breaks down
- Fix: Use Bellman-Ford (DP) - but check size constraints
π« Using DFS for shortest path
- Problem: DFS explores all paths (exponential)
- Fix: Use BFS (unweighted) or Dijkstra (weighted)
π« Ignoring size constraints
- Problem: O(VΒ²) algorithm on 10^5 nodes β TLE
- Fix: Always calculate actual operations: if V=10^5, VΒ²=10^10 (too slow)
π« Assuming Greedy always works
- Problem: Many graph problems need complete exploration
- Fix: Prove greedy property or use BFS/DFS
# Practical Performance Benchmarks
| Operations | Time Limit 1s | Time Limit 2s | Typical Memory |
|---|---|---|---|
| 10^6 | β Fast | β Fast | ~4MB |
| 10^7 | β OK | β Fast | ~40MB |
| 10^8 | β οΈ Tight | β OK | ~400MB |
| 10^9 | β TLE | β οΈ Tight | ~4GB (exceeds limit) |
| 10^10 | β TLE | β TLE | β MLE |
Key Insight:
- If your algorithm has O(VΒ²) or O(VE) and V > 10^4, consider greedy alternative
- If E > 10^5 and you need MST, only greedy (Kruskal/Prim) will work
- Space complexity matters: O(VΒ²) adjacency matrix fails when V > 10^4 (100MB+)
# Summary: Algorithm Selection Matrix
| Problem Goal | Size < 10^3 | 10^3 β€ Size β€ 10^5 | Size > 10^5 | Algorithm |
|---|---|---|---|---|
| Shortest path (unweighted) | BFS | BFS | BFS (if feasible) | O(V+E) |
| Shortest path (weighted +) | Dijkstra | Dijkstra | Dijkstra required | O((V+E)logV) |
| Shortest path (weighted -) | Bellman-Ford | Bellman-Ford | Usually infeasible | O(VE) |
| MST | Any greedy | Kruskal/Prim | Kruskal/Prim required | O(ElogE) |
| All paths | DFS | DFS (if small) | Usually infeasible | O(V! or 2^V) |
| Connectivity | BFS/DFS/UF | Union-Find | Union-Find | O(EβΊ(V)) |
| Cycle detection | DFS | DFS | DFS | O(V+E) |
| Topological sort | DFS/BFS | DFS/BFS | DFS/BFS | O(V+E) |
Bottom Line:
- Size > 10^5 β Greedy is often required (if applicable)
- Need optimal path β Greedy if proven, BFS/DFS otherwise
- Need all solutions β BFS/DFS (but check if feasible)
- When in doubt β Calculate operations: if > 10^8, find better algorithm
# 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)