Greedy Algorithms: Making Locally Optimal Choices

Learn when greedy algorithms work, how to prove correctness using exchange arguments, and common greedy patterns.

published: reading time: 24 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

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

PatternGreedy ChoiceExample
Sort by finish timeEarliest finishing firstActivity selection
Sort by ratioHighest value/weightFractional knapsack
Priority queueSmallest/largest availableDijkstra, Prim’s
Two pointersMove pointer with smaller valueWater container

Trade-off Analysis

AspectGreedyDynamic Programming
TimeOften O(n log n)Often O(n²) or worse
SpaceO(1) to O(n)O(n) to O(n²)
CorrectnessProblem-specific proofAlways correct if substructure holds
FlexibilityLowHigh
ImplementationUsually simpleCan 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:

PriorityAttributeAttacker-controllable?Notes
1Smallest resource IDNoImmutable after creation
2Earliest submissionYes, but timestampedUse server time, not client-supplied time
3Lexicographically smallest nameDependsStable for fixed-purpose resources
LastReject ambiguous inputN/ASafe 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 CheckReasonable ThresholdRationale
Max character frequency ratio80% of total charactersSingle character dominating indicates crafted input
Max tree depthlog2(n_characters) * 2Natural Huffman tree depth is O(log n); 2x is safe limit
Min total characters1Reject empty input gracefully
Max single character counttotal_chars * 0.95Leaves 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 RuleExample ThresholdSuspicious Pattern
Max value-to-weight ratio1000x the median ratioRatios 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 ratiosRatios matching exact integersAttackers often pick round numbers
Rate limitingN items per time windowPrevents 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:

  1. Safe choice lemma: Prove the greedy choice is safe — there exists an optimal solution that contains it.
  2. Reduction: Show that after making the greedy choice, the remaining subproblem is of the same type.
  3. 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:

ApproachWhen to UseCost
Formal proofBefore deployment, alwaysHigh effort, but one-time
Runtime validationWhen proof is intractable or problem is modifiableO(n) per run for small n, practical for test harness
A/B comparisonReplacing an existing algorithm in productionRun both, compare outputs, alert on divergence
Stress testingDuring development, random adversarial inputsGenerate 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
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

  1. 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.

  2. 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.

  3. 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.value vs b.weight * a.value instead of dividing).

  4. 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

1. How do you prove a greedy algorithm is correct?

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.

2. What's the difference between Prim's and Kruskal's MST algorithms?

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.

3. Why does greedy fail for 0/1 knapsack but work for fractional knapsack?

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.

4. When should you choose greedy over dynamic programming?

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.

5. Explain Dijkstra's algorithm and why it's greedy.

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
6. What is the greedy-choice property and optimal substructure?

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
7. How does Huffman coding demonstrate greedy behavior?

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
8. Describe the fractional knapsack problem and how greedy solves it.

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
9. What is the time complexity of activity selection and why?

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
10. How does the greedy approach work for job sequencing with deadlines?

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
11. Explain the "water container" problem and its greedy solution.

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
12. What is the role of priority queues in greedy algorithms?

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)
13. How do you handle tie-breaking in greedy algorithms?

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
14. When can greedy fail even when the problem seems to have the right properties?

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
15. Compare greedy vs divide-and-conquer for optimization problems.

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
16. What is the connection between greedy and matroid theory?

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
17. How does Kruskal's algorithm handle cycle detection?

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
18. What are the key differences between Prim's and Prim's algorithms for MST?

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
19. How do you validate greedy results against known optimal solutions?

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
20. Describe a scenario where greedy is asymptotically optimal but practically unusable.

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.

#binary-search #algorithms #searching

Knapsack Problem Variants

Master the 0/1, unbounded, and fractional knapsack variants with DP solutions and optimization techniques for resource allocation problems.

#knapsack #dynamic-programming #algorithms

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.

#dynamic-programming #memoization #tabulation