Greedy Algorithms: Making Locally Optimal Choices
Learn when greedy algorithms work, how to prove correctness using exchange arguments, and common greedy patterns.
Greedy algorithms build solutions by making locally optimal choices at each step, relying on sorting and exchange arguments to prove convergence to global optimality. Common greedy patterns include activity selection for scheduling, Huffman coding for compression, and minimum spanning tree algorithms—typically achieving O(n log n) complexity. The critical first step is proving the greedy choice property holds for your problem, otherwise the algorithm produces suboptimal results. Master this proof technique and you'll have a powerful tool for optimization problems where divide-and-conquer or dynamic programming would be overkill.
Greedy Algorithms: Making Locally Optimal Choices
Introduction
Greedy algorithms are the simplest algorithmic paradigm: at each step, make the choice that looks best right now, never reconsidering past decisions. The challenge is proving this local optimality leads to global optimality.
The greedy property must hold: there exists an optimal solution that contains the greedy choice at each step. When this property holds, greedy algorithms typically run in O(n log n) or O(n) after sorting.
Activity Selection Problem
The classic greedy problem: select the maximum number of non-overlapping activities:
def activity_selection(activities):
"""
Select maximum set of non-overlapping activities.
Args:
activities: List of (start, end) tuples
Returns:
List of selected activity indices
"""
# Sort by finish time (key greedy choice!)
sorted_activities = sorted(enumerate(activities), key=lambda x: x[1][1])
selected = [sorted_activities[0][0]]
last_end = sorted_activities[0][1][1]
for idx, (start, end) in sorted_activities[1:]:
if start >= last_end: # Non-overlapping with last selected
selected.append(idx)
last_end = end
return selected
Huffman Coding
Greedy compression: build optimal prefix-free codes:
import heapq
from collections import Counter
class HuffmanNode:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
def __lt__(self, other):
return self.freq < other.freq
def huffman_coding(text):
"""Build Huffman coding tree and return code mapping."""
if not text:
return {}
# Build initial min-heap of characters
freq = Counter(text)
heap = [HuffmanNode(char, f) for char, f in freq.items()]
heapq.heapify(heap)
# Build tree by merging two minimum frequency nodes
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left, merged.right = left, right
heapq.heappush(heap, merged)
root = heap[0]
# Traverse to build codes
codes = {}
def traverse(node, code):
if node.char is not None:
codes[node.char] = code or '0' # Handle single character
else:
traverse(node.left, code + '0')
traverse(node.right, code + '1')
traverse(root, '')
return codes
When Greedy Works
Greedy is correct when:
- The problem has optimal substructure AND the greedy choice doesn’t preclude optimal solutions
- A greedy choice property holds: there’s an optimal solution that makes the greedy choice
- Proof via exchange argument: any optimal solution can be transformed to the greedy solution without worsening the outcome
Common greedy problems:
- Activity selection (sort by finish time)
- Fractional knapsack (sort by value/weight ratio)
- Huffman coding (merge smallest frequencies)
- Minimum spanning tree (Prim’s, Kruskal’s)
- Dijkstra’s shortest path (select minimum distance vertex)
When Greedy Fails
Don’t use greedy when:
- Local optimum doesn’t guarantee global optimum
- Discrete choices where taking “a bit more” now blocks better options later
- Problems requiring backtracking to undo early decisions
Counter-example: 0/1 knapsack where items can’t be divided—greedy by ratio fails.
Proving Greedy Correctness
def prove_greedy_correct(problem):
"""
Template for proving greedy correctness via exchange argument:
1. Claim: There exists an optimal solution that makes the greedy choice
2. Assume an optimal solution O differs at first greedy choice
3. Exchange: Replace O's choice with the greedy choice
4. Show: The exchange doesn't make the solution worse
5. Repeat for subsequent choices
"""
pass
Common Greedy Patterns
| Pattern | Greedy Choice | Example |
|---|---|---|
| Sort by finish time | Earliest finishing first | Activity selection |
| Sort by ratio | Highest value/weight | Fractional knapsack |
| Priority queue | Smallest/largest available | Dijkstra, Prim’s |
| Two pointers | Move pointer with smaller value | Water container |
Trade-off Analysis
| Aspect | Greedy | Dynamic Programming |
|---|---|---|
| Time | Often O(n log n) | Often O(n²) or worse |
| Space | O(1) to O(n) | O(n) to O(n²) |
| Correctness | Problem-specific proof | Always correct if substructure holds |
| Flexibility | Low | High |
| Implementation | Usually simple | Can be complex |
Common Pitfalls / Anti-Patterns
Greedy algorithms have specific security concerns when input ordering or values are attacker-controlled.
Greedy choice manipulation via crafted input ordering
The danger isn’t just in which choice the algorithm makes, it’s in which choice it can be forced to make. When an attacker controls input order, they can manipulate tie-breaking behavior to steer the greedy solution toward a specific outcome. In activity selection, if multiple activities share the same finish time, the order you check them determines which one gets picked first. An attacker who can reorder inputs can exploit this to reserve specific time slots for themselves while blocking legitimate users.
Consider a meeting room booking system that sorts available rooms by finish time. If an attacker submits bookings with identical end times, the order those bookings appear in the sorted list determines which room gets assigned. The attacker could craft bookings with end times matching the most desirable room, then submit them in a sequence that forces the algorithm to select it over others. This is a real concern in any system where greedy allocation handles shared resources—conference rooms, compute instances, reservation windows.
Deterministic tie-breaking breaks this attack vector. Before running the greedy selection, sort ties by a stable attribute the attacker cannot influence, such as the smallest resource ID or earliest submission timestamp. Document the tie-breaking rule in the system design and log which ties occurred during allocation so anomalies can be detected. If multiple equivalent solutions exist, either reject the ambiguous input or return all valid options rather than picking one arbitrarily.
A tie-breaking strategy that works in practice chains multiple stable attributes together, in order:
| Priority | Attribute | Attacker-controllable? | Notes |
|---|---|---|---|
| 1 | Smallest resource ID | No | Immutable after creation |
| 2 | Earliest submission | Yes, but timestamped | Use server time, not client-supplied time |
| 3 | Lexicographically smallest name | Depends | Stable for fixed-purpose resources |
| Last | Reject ambiguous input | N/A | Safe fallback when ties cannot be resolved |
Log tie events. The log should capture the tied candidates, which one was chosen, and which rule resolved the tie. A sudden spike in ties on a popular resource is worth investigating.
def activity_selection_secure(activities):
"""
Select maximum set of non-overlapping activities with deterministic tie-breaking.
Args:
activities: List of (start, end, resource_id) tuples
"""
# Sort by finish time, then by resource_id for deterministic tie-breaking
sorted_activities = sorted(
enumerate(activities),
key=lambda x: (x[1][1], x[1][2]) # (finish_time, resource_id)
)
selected = [sorted_activities[0][0]]
last_end = sorted_activities[0][1][1]
for idx, (start, end, _) in sorted_activities[1:]:
if start >= last_end:
selected.append(idx)
last_end = end
return selected
Input ordering attacks on Huffman coding
Huffman coding’s security surface is often overlooked because the algorithm itself is information-theoretically secure. The vulnerability isn’t in the compression—it’s in the tree structure the algorithm produces. If an attacker controls character frequencies in the input, they can craft inputs that create specific tree shapes. A decoder that processes the resulting tree without sufficient validation can be driven into pathological behavior.
A malformed tree with extreme depth (say, a chain of single-child nodes) can cause a naive decoder to recurse deeply, overflow the call stack, or consume unbounded memory while traversing. This is especially dangerous in any system where Huffman-decoded data is fed directly into a parser or renderer without bounds checking. The attacker doesn’t need to know the decoder’s internals, they just need to produce frequencies that force a specific tree topology.
Defensive measures focus on tree structure validation rather than preventing the attack outright. Before building the tree, validate that frequency counts are within expected ranges—reject inputs where any single character represents an unreasonably large fraction of the total. Set a maximum tree depth limit and reject any tree that exceeds it. Many Huffman implementations in production libraries already enforce depth limits; if yours doesn’t, add a depth counter that aborts tree construction after N levels.
Practical thresholds for most text compression use cases look like this:
| Validation Check | Reasonable Threshold | Rationale |
|---|---|---|
| Max character frequency ratio | 80% of total characters | Single character dominating indicates crafted input |
| Max tree depth | log2(n_characters) * 2 | Natural Huffman tree depth is O(log n); 2x is safe limit |
| Min total characters | 1 | Reject empty input gracefully |
| Max single character count | total_chars * 0.95 | Leaves room for at least one other character |
Before decoding, validate the tree structure:
def validate_huffman_tree(node, depth=0, max_depth=32):
"""
Validate Huffman tree structure before use.
Aborts if tree is malformed or exceeds safe depth.
"""
if depth > max_depth:
raise ValueError(f"Huffman tree exceeds maximum depth of {max_depth}")
if node.char is not None:
return # Leaf node
if node.left is None or node.right is None:
raise ValueError("Huffman tree has missing child nodes")
validate_huffman_tree(node.left, depth + 1, max_depth)
validate_huffman_tree(node.right, depth + 1, max_depth)
Set max_depth to match your input size expectations. For 256 distinct byte values, a depth of 16 handles any valid tree. If your encoder/decoder is recursive, depth limits also prevent stack overflow from crafted trees.
Ratio manipulation in fractional knapsack
Fractional knapsack is theoretically safe—greedy by value/weight ratio always produces the optimal solution. But the theory assumes an honest input space. In practice, if an attacker controls item values and weights, they can craft ratios that steer the greedy selection toward items the attacker wants selected, potentially bypassing capacity constraints in ways that violate the system’s intent.
Imagine a resource allocation system where items represent compute credits with associated costs. An attacker could assign extremely high value-to-weight ratios to specific items by inflating their declared value. The greedy algorithm would pack those items first, consuming capacity that should have gone to legitimate high-value items. This matters whenever the “value” in your system reflects something an attacker can influence: user-reported priorities, bid amounts, declared resource requirements.
Input validation is the primary defense. Reject items where the value-to-weight ratio exceeds a configurable maximum, or where the ratio is suspiciously round (like exactly 10.0). Set floor and ceiling bounds on both weight and value per item based on your application’s expected range. If items represent monetary or computational resources, cross-validate declared values against observable behavior. A user claiming an item has value 1000x higher than any other should be required to justify that claim or be rate-limited.
Validation rules that catch most practical attacks:
| Validation Rule | Example Threshold | Suspicious Pattern |
|---|---|---|
| Max value-to-weight ratio | 1000x the median ratio | Ratios exceeding this indicate inflation |
| Min/max weight range | [0.001, capacity * 0.5] | Weights outside this range are unrealistic |
| Min/max value range | [0.01, capacity * max_value] | Values must fit within knapsack capacity |
| Suspiciously round ratios | Ratios matching exact integers | Attackers often pick round numbers |
| Rate limiting | N items per time window | Prevents bulk crafting of malicious items |
def validate_fractional_knapsack_items(items, capacity):
"""
Validate knapsack items before running greedy selection.
Raises ValueError on any suspicious input.
"""
if not items:
return # Empty is valid, caller handles it
values = [item.value for item in items]
weights = [item.weight for item in items]
median_ratio = sorted([v / w for v, w in zip(values, weights) if w > 0])[len(items) // 2]
max_ratio = median_ratio * 1000
for item in items:
if item.weight <= 0 or item.weight > capacity * 0.5:
raise ValueError(f"Weight {item.weight} outside valid range")
if item.value <= 0:
raise ValueError(f"Value {item.value} must be positive")
ratio = item.value / item.weight
if ratio > max_ratio:
raise ValueError(f"Value/weight ratio {ratio:.2f} exceeds safe maximum {max_ratio:.2f}")
# Check for suspiciously round ratios (attacker convenience)
if ratio == int(ratio) and ratio > median_ratio * 10:
raise ValueError(f"Suspiciously round ratio {ratio} detected")
Cross-validation catches inflation that static bounds miss. If items represent compute credits, compare declared costs against historical averages for that user. A user suddenly claiming items worth 50x their historical average should be flagged for review before the allocation runs.
Proof absence exploited
The most insidious greedy failure isn’t a coding bug—it’s a missing proof. A greedy algorithm implemented without a correctness proof is a time bomb. It might work on your test cases, pass code review, and deploy successfully. Then an attacker finds an input where the locally optimal choice doesn’t lead to a global optimum, and the system returns a suboptimal result as if it were correct.
This risk is highest when greedy replaces a provably correct but slower algorithm. The developer benchmarking the new system notices greedy is 10x faster and swaps out the exhaustive search. Nobody writes a proof because the algorithm “looks obviously correct” — activity selection by finish time feels intuitive. But intuition fails in edge cases. The 0/1 knapsack problem feels like it should have a greedy solution too, and people get burned by it every year.
A correctness proof for greedy does two things: it identifies the exact condition under which the greedy-choice property holds, and it shows that any optimal solution can be transformed into the greedy solution without worsening the outcome. If you can’t write down that proof, you don’t understand the algorithm well enough to deploy it. Fall back to a slower but correct implementation, or add runtime validation that compares greedy output against a brute-force solution on representative test inputs before accepting the result.
The standard proof structure for greedy uses an exchange argument with three steps:
- Safe choice lemma: Prove the greedy choice is safe — there exists an optimal solution that contains it.
- Reduction: Show that after making the greedy choice, the remaining subproblem is of the same type.
- Induction: Combine the safe choice with induction on subproblem size to conclude optimality.
For activity selection, the safe choice lemma is: “There exists an optimal solution that includes the activity with the earliest finish time.” The proof swaps any activity that finishes later with the earliest-finishing activity — the swap never reduces the number of selected activities. The reduction is immediate: after selecting an activity, the remaining unconflicted activities form the same subproblem.
If the proof feels intractable, runtime validation is a practical alternative. Run greedy alongside a brute-force or DP solver on a sample of inputs and compare outputs:
| Approach | When to Use | Cost |
|---|---|---|
| Formal proof | Before deployment, always | High effort, but one-time |
| Runtime validation | When proof is intractable or problem is modifiable | O(n) per run for small n, practical for test harness |
| A/B comparison | Replacing an existing algorithm in production | Run both, compare outputs, alert on divergence |
| Stress testing | During development, random adversarial inputs | Generate random inputs, verify greedy matches brute-force |
def verify_greedy_against_bruteforce(problem, greedy_fn, brute_force_fn, samples=100):
"""
Randomly test greedy against brute-force for small problem sizes.
Prints warnings but does not raise — caller decides how to handle mismatches.
"""
import random
mismatches = 0
for _ in range(samples):
input_data = problem.generate_random()
greedy_result = greedy_fn(input_data)
brute_result = brute_force_fn(input_data)
if greedy_result != brute_result:
mismatches += 1
print(f"MISMATCH on input: {input_data}")
print(f" Greedy: {greedy_result}, Brute-force: {brute_result}")
if mismatches == 0:
print(f"All {samples} samples matched.")
else:
print(f"{mismatches}/{samples} mismatches detected.")
return mismatches == 0
Greedy Choice vs Exhaustive Search
graph TD
A["Problem input"] --> B{"Problem has greedy-choice property?"}
B -->|Yes| C["Make locally optimal choice"]
C --> D{"Recurse or iterate?"}
D -->|Iterate| E["Make next greedy choice"]
E --> F{"All choices made?"}
F -->|No| E
F -->|Yes| G["Return solution Globally optimal"]
B -->|No| H["Use exhaustive search or dynamic programming"]
H --> I["Explore all possibilities Find optimal solution"]
Quick Recap Checklist
- Check if problem has greedy-choice property
- Prove via exchange argument or cut-and-paste
- Sort appropriately (usually by some key)
- Iterate making locally optimal choices
- Handle edge cases (empty input, single element)
- Verify no counter-examples exist
Production Failure Scenarios
-
Greedy choice not being globally optimal: This is the most common greedy failure—applying greedy to a problem where the local optimum doesn’t guarantee a global optimum. The 0/1 knapsack problem is the classic example: greedy by value/weight ratio fails because taking a high-ratio item can block you from multiple lower-ratio items. Always prove correctness via exchange argument before implementing, and log when greedy produces suboptimal results in test cases.
-
Lack of optimal substructure: Even when the greedy choice property holds locally, the problem might not have optimal substructure—the optimal solution doesn’t consist of optimal solutions to subproblems. Without optimal substructure, greedy cannot work even if individual choices seem correct.
-
Floating-point comparisons causing incorrect ordering: When sorting by ratios (like value/weight in fractional knapsack), floating-point precision can cause items that should be equal to sort in unpredictable orders. This leads to non-deterministic behavior. Use integer arithmetic where possible (e.g., compare
a.weight * b.valuevsb.weight * a.valueinstead of dividing). -
Integer overflow in priority calculations: For Huffman coding with very large frequencies or Dijkstra with large weights, accumulating values in priority queues can overflow. Use bounded integer types or detect overflow before it happens.
Interview Questions
The standard method is exchange argument: Show that any optimal solution can be transformed into the greedy solution without worsening its value. You prove that at each step, the greedy choice is safe—replacing whatever the optimal solution does at that step with the greedy choice doesn't break optimality. Alternatively, prove via induction: the greedy solution is optimal for the first k choices implies it's optimal for choice k+1.
Both are greedy, but they grow differently. Kruskal's looks at all edges globally, sorts them, and adds the cheapest edge that doesn't create a cycle (using union-find). Prim's grows one tree from a start vertex, always adding the cheapest edge connecting the tree to an outside vertex (using a priority queue). Kruskal's is often simpler; Prim's can be faster for dense graphs.
In fractional knapsack, you can take any fraction of an item, so taking the item with best value/weight ratio is always safe. But in 0/1 knapsack, you must take whole items. Taking an item with high ratio might block you from taking multiple smaller items that together exceed its value. The greedy choice creates a local optimum that isn't global. This requires DP to solve correctly.
Choose greedy when you can prove the greedy-choice property holds and you need efficiency. Greedy is O(n log n) or better; DP is often O(n²) or worse. Choose DP when the problem doesn't have the greedy-choice property but does have optimal substructure and overlapping subproblems. When in doubt, try greedy first (it's simple to implement)—if you find a counterexample, fall back to DP.
Expected answer points:
- Dijkstra finds shortest paths from a source vertex to all other vertices in a weighted graph with non-negative edge weights
- It's greedy because it always selects the unvisited vertex with smallest known distance, locks in that distance permanently, and never revisits it
- Uses a priority queue (min-heap) to efficiently find the minimum-distance vertex
- Time complexity: O((V+E) log V) with binary heap implementation
- Fails with negative edge weights—use Bellman-Ford instead
Expected answer points:
- Greedy-choice property: A globally optimal solution can be built by making locally optimal choices at each step—there's always an optimal solution that contains the greedy choice
- Optimal substructure: An optimal solution to the problem contains within it optimal solutions to subproblems
- Both properties must hold for greedy to work; optimal substructure alone is necessary but not sufficient (DP also needs it)
- Example: In activity selection, choosing the earliest-finishing activity leaves a subproblem (remaining activities) that is itself an activity selection problem
Expected answer points:
- Builds an optimal prefix-free binary tree for character compression
- Greedy step: repeatedly merge the two nodes with smallest frequency using a min-heap
- This locally optimal merge step (smallest two frequencies) leads to globally optimal codingtree
- Proof uses exchange argument: any optimal tree can be transformed to one where the two least-frequent characters are siblings at the bottom
- Results in variable-length encoding—frequent characters get short codes, rare characters get longer codes
Expected answer points:
- Given items with weights and values, maximize value in a knapsack with capacity W—you can take fractions of items
- Greedy solution: sort items by value/weight ratio descending, take items whole until capacity nearly reached, then take fraction of last item
- Correct because taking more of a higher-ratio item always improves the solution—no blocking effect since fractions are allowed
- Time complexity: O(n log n) for sorting
- Contrast with 0/1 knapsack where greedy fails
Expected answer points:
- Activity selection finds maximum number of non-overlapping intervals
- Sort activities by finish time: O(n log n)
- Single pass to select activities: O(n)
- Total: O(n log n)
- Can be implemented in O(n) if already sorted by finish time
Expected answer points:
- Given jobs with profits and deadlines, sequence jobs to maximize total profit, only one job at a time
- Sort jobs by profit descending (greedy by highest value first)
- For each job, place it in the latest available time slot before its deadline using a union-find or slot array
- Proof: taking highest profit job that fits doesn't block more total profit since lower-profit jobs fill earlier slots
- Time complexity: O(n log n) for sorting plus O(n × deadline) for placement
Expected answer points:
- Given height array representing container walls, find max water area between two walls
- Greedy: two pointers at ends, move the pointer with smaller height inward
- Why it works: area is limited by shorter wall; moving the shorter wall might find a taller one that increases area
- Proof by contradiction: any optimal solution can be transformed to one using the two-pointer approach
- Time complexity: O(n) with two pointers, much better than O(n²) brute force
Expected answer points:
- Priority queues (min-heaps or max-heaps) efficiently support the "select best available" operation central to greedy
- Used in Dijkstra's, Prim's MST, Huffman coding—allows O(log n) selection of minimum/maximum
- Alternatives: sorting (O(n log n) preprocessing then O(1) selection), arrays for small inputs
- Binary heap provides balance of efficiency and simplicity; Fibonacci heap gives theoretical improvement for Dijkstra
- Key tradeoff: lazy deletion (mark invalid, remove when popped) vs eager deletion (update on change)
Expected answer points:
- Ties can lead to multiple valid greedy solutions—consistency matters for determinism
- When sorting by key (e.g., finish time), add secondary sort key (e.g., start time or ID) for stability
- In priority queues, define consistent tie-breaking in comparator
- Security implication: documented tie-breaking prevents adversarial manipulation
- Example: activity selection with same finish time—pick the one with earliest start time or smallest index
Expected answer points:
- Subtle edge cases: the greedy-choice property may hold for small cases but fail at scale
- Hidden dependencies: greedy choice might affect future choices in non-obvious ways
- Counter-example discovery: try adversarial inputs, especially ones where items have equal values
- Proof gaps: lack of formal proof means edge cases may invalidate the approach
- Recommendation: implement both greedy and DP, compare on random test cases before deployment
Expected answer points:
- Greedy: Make local choice, reduce to subproblem, never revisit
- Divide-and-conquer: Split problem, solve subproblems independently, combine results
- Greedy is top-down with irreversible choices; divide-and-conquer often has recomputation
- Greedy examples: activity selection, Huffman coding, Dijkstra
- Divide-and-conquer examples: merge sort, closest pair of points, Strassen's matrix multiplication
- Some problems fit both paradigms but need different insights
Expected answer points:
- A matroid is an independence system satisfying hereditary and exchange properties
- Greedy optimally solves any problem where feasible set forms a matroid and objective is additive
- Examples: spanning trees (graphic matroids), linearly independent vectors (linear matroids)
- Provides formal framework: if you can prove your problem's feasible solutions form a matroid, greedy is guaranteed correct
- Trade-off: many problems don't form matroids but still admit greedy—need case-by-case analysis
Expected answer points:
- Kruskal's: sort all edges, iterate adding cheapest edge that doesn't create a cycle
- Cycle detection uses Union-Find (Disjoint Set Union) data structure
- Each vertex starts in its own set; adding an edge merges two sets
- Before adding edge (u,v), check if find(u) == find(v)—if true, edge creates cycle
- Union-Find with path compression and union by rank achieves near-constant amortized time
- Overall complexity: O(E log E) dominated by sorting
Expected answer points:
- Prim's: Grows one tree from a starting vertex, always adding cheapest edge connecting tree to new vertex (like Dijkstra but without path length accumulation)
- Kruskal's: Grows forest of trees, adding cheapest edge that connects any two different trees
- Prim's uses a frontier priority queue; Kruskal's processes all edges sorted by weight
- Prim's better for dense graphs (O(V²) with array, O(E log V) with heap)
- Kruskal's better for sparse graphs O(E log E)
- Both guarantee MST; choice depends on graph density and implementation
Expected answer points:
- Generate random test cases and compare greedy output to exhaustive/DP solution for small n
- Use known benchmark instances from literature where optimal values are documented
- Track solution quality ratio: greedy_result / optimal_result; should be close to 1.0
- Profile corner cases: clustered values, equal weights, adversarial orderings
- Statistical testing: run many instances, report minimum/median/5th percentile quality
- For production: continuously log quality metrics, alert if quality drops below threshold
Expected answer points:
- Greedy might have O(n) or O(n log n) complexity but constants make it slow for realistic n
- Example: a greedy algorithm requiring expensive data structure operations per step
- Alternatively: greedy might be simple to state but implementation complexity is high
- Memory pressure: if greedy requires storing all possible choices, memory dominates runtime
- Consider: for small n, simpler O(n²) algorithm outperforms optimized O(n log n) greedy due to lower constant factors
- Recommendation: benchmark with realistic data sizes, not just asymptotic analysis
Further Reading
- [Cormen et al., Introduction to Algorithms — Greedy Algorithms chapter]
- [GeeksforGeeks — Greedy Algorithms]
- [Visualgo — Algorithm visualization]
Conclusion
Greedy algorithms make locally optimal choices at each step without reconsidering past decisions, achieving O(n log n) or better when the greedy-choice property holds. Prove correctness via exchange argument: any optimal solution can be transformed into the greedy solution without worsening the outcome. Common greedy problems include activity selection (sort by finish time), fractional knapsack (sort by value/weight ratio), Huffman coding (merge smallest frequencies), and minimum spanning tree algorithms. When greedy fails—typically in 0/1 knapsack—fall back to dynamic programming.
Category
Related Posts
Binary Search Variants: Beyond Simple Lookup
Master variations of binary search including lower bound, upper bound, search in rotated array, and fractional searching for optimization problems.
Knapsack Problem Variants
Master the 0/1, unbounded, and fractional knapsack variants with DP solutions and optimization techniques for resource allocation problems.
Memoization vs Tabulation: Two Faces of Dynamic Programming
Understand the two fundamental DP approaches—top-down with memoization and bottom-up with tabulation—plus hybrid techniques like the/m on the fly.