Loading content...
In the previous page, we cataloged classic problems where greedy fails. But in practice, you'll encounter new problems—problems that don't appear in textbooks, problems where you've invented a greedy strategy and need to verify whether it works.
How do you know if your greedy algorithm is correct? You have two options:
This page focuses on the second skill: the systematic construction of counterexamples. This is not merely a debugging technique—it's a fundamental algorithmic skill that develops your intuition for problem structure and safeguards you from deploying incorrect solutions.
By the end of this page, you will be able to systematically construct counterexamples that break incorrect greedy algorithms, understand the adversarial mindset required for counterexample generation, and apply structured techniques to expose greedy failures in novel problems.
Proving correctness is hard—you must show the algorithm works for ALL inputs. Finding a counterexample is easier—you only need ONE input where it fails. But "easier" doesn't mean "easy." Counterexamples for subtle algorithms can be deceptively hard to construct.
Constructing counterexamples requires a fundamental shift in perspective. Instead of asking "How would I solve this problem?", you must ask "How can I construct an input where the greedy approach takes a wrong turn?"
This is adversarial thinking. You are the antagonist trying to break the algorithm. You control the input and can craft it maliciously to exploit the algorithm's blind spots.
The Greedy Algorithm's Vulnerabilities:
Every greedy algorithm has the same structural weakness: it makes irrevocable decisions based on incomplete information. Your goal as an adversary is to:
Always aim for the SMALLEST counterexample possible. A counterexample with 3 items is more convincing and debuggable than one with 100 items. Start small: can you break the algorithm with 2 items? 3? Find the minimal structure that causes failure.
Rather than relying on pure intuition, follow this structured process to generate counterexamples systematically:
Worked Example: Breaking a Task Scheduling Greedy
Suppose you've designed a greedy algorithm for task scheduling:
Proposed Greedy: Always select the highest-profit unscheduled task that can still meet its deadline.
Let's try to break it:
Step 1: Identify the Greedy Criterion
The algorithm sorts by profit (descending) and greedily assigns tasks.
Step 2: Identify What's Ignored
The algorithm ignores deadline flexibility. A high-profit task with a late deadline is treated the same as a high-profit task with an early deadline. But early-deadline tasks constrain scheduling more.
Step 3: Create the Trap
Create a high-profit task with an early deadline that, if taken, blocks other valuable tasks.
Step 4: Create the Hidden Gem Alternative
Step 5: Verify
Greedy Execution:
Greedy total: 100 + 60 = 160
Optimal:
Wait, greedy wins! Let's adjust:
Greedy: A + B = 50 + 40 = 90 Optimal: B + C = 40 + 40 = 80
Greedy still wins... The issue is that profit-based greedy actually works for this problem variant! This is a case where our counterexample attempt reveals the algorithm might be correct. Let's try a different attack.
Refined Attack:
Consider tasks with both profits AND weights, where we maximize profit/weight ratio... Actually, let's attack a simpler target: greedy sorted by deadline alone (earliest deadline first).
Greedy (earliest deadline):
Total: 101 — This is optimal!
Let's try:
Greedy (earliest deadline first): Both A and B have deadline 1. If we tie-break arbitrarily, say we take A:
Greedy Total: 1 + 1 = 2
Optimal: Take B in slot 1, C in slot 2 Optimal Total: 100 + 1 = 101
Counterexample found! The earliest-deadline greedy ignores profit, leading to arbitrarily bad solutions.
We successfully broke the earliest-deadline-first greedy. The counterexample is minimal (3 tasks) and demonstrates the flaw clearly: greedy ignores profit entirely, taking a profit-1 task over a profit-100 task simply because of an arbitrary tie-break.
Certain counterexample structures appear repeatedly across different greedy algorithms. Recognizing these patterns accelerates counterexample construction.
Structure: One item that looks attractive but, if taken, blocks multiple better items.
Template:
Example (Interval Scheduling):
Suppose greedy picks the longest interval first.
Blocker: [=========long interval=========] (length 10)
Blocked 1: [--] (length 2)
Blocked 2: [--] (length 2)
Blocked 3: [--] (length 2)
Blocked 4: [--] (length 2)
Blocked 5: [--] (length 2)
Greedy takes the length-10 blocker (1 interval). Optimal takes all five short intervals (5 intervals).
Counterexample! Longest-first greedy fails for interval scheduling.
Structure: Greedy fills capacity with one 'good' item, leaving no room for the optimal combination.
Template:
Example (Knapsack with capacity 10):
Greedy by ratio: Takes A (ratio 5.6), then Trap doesn't fit (6 > 5 remaining), B doesn't fit (5 = 5, taking B), leaving capacity 0.
Actually greedy by ratio takes A=28, then considers B with 5 capacity remaining, and B fits! Total = 56.
Let's fix:
Greedy by ratio: Takes A (ratio 5.2), remaining capacity = 5, B fits (ratio 5.2), total = 52. Trap alternative: Take Trap (weight 7, value 35), remaining = 3, neither A nor B fits. Total = 35.
Greedy wins here too. The 0/1 knapsack is surprisingly robust against simple counterexamples. Let's try the canonical one:
Greedy by ratio: A (60, cap=40), B (100, cap=20), C doesn't fit. Total = 160. Optimal: B + C = 220.
Here's the counterexample!
To construct a capacity trap: make the trap item have excellent ratio but awkward size, and make optimal items have slightly worse ratios but sizes that combine perfectly to fill capacity. The key is sizing the trap to leave insufficient room for alternatives.
Structure: A short-term sacrifice enables a long-term gain that greedy cannot foresee.
Template:
Example (Path Selection):
Find shortest path A → D:
1 100
A ----→ B -------→ D
| ↗
| /
10 1 /
| C ----
| ↗
+---
Edges: A→B (weight 1), B→D (weight 100), A→C (weight 10), C→D (weight 1)
Greedy (at each step, take shortest edge):
Optimal:
The short-term sacrifice (taking the longer A→C edge) enables the much shorter C→D edge. Greedy's commitment to A→B forecloses this path.
Structure: Greedy picks the largest unit, but smaller units combine more efficiently.
Template:
Example (Coin Change with {1, 3, 4}, target = 6):
Greedy: 4 + 1 + 1 = 3 coins Optimal: 3 + 3 = 2 coins
The denominations 3 and 3 cover the target exactly, while 4 leaves remainder 2 that requires two 1s.
Structure: In graph problems, a high-degree vertex looks valuable but blocks many neighbors.
Template:
Example (Vertex Cover selecting min-degree vertices first):
Star graph: Center X connected to leaves A, B, C, D, E.
Greedy (min degree): Select all leaves (degree 1 each) → 5 vertices Optimal: Select only X → 1 vertex covers all edges
Or for Maximum Independent Set (max weight, as we saw):
Hub X (weight 15), Leaves A-E (weight 10 each).
Greedy (max weight): Select X → total 15 Optimal: Select all leaves → total 50
A counterexample is only valid if:
Let's formalize the verification process:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
def verify_knapsack_counterexample(): """ Rigorous verification of the 0/1 knapsack counterexample. """ # Input definition items = [ {'name': 'A', 'weight': 10, 'value': 60}, {'name': 'B', 'weight': 20, 'value': 100}, {'name': 'C', 'weight': 30, 'value': 120}, ] capacity = 50 # Step 1: Constraint Check print("=== Step 1: Constraint Verification ===") for item in items: assert item['weight'] > 0, f"{item['name']} has non-positive weight" assert item['value'] > 0, f"{item['name']} has non-positive value" assert capacity > 0, "Capacity must be positive" print("✓ All constraints satisfied\n") # Step 2: Greedy Execution Trace print("=== Step 2: Greedy Algorithm Trace ===") # Sort by value/weight ratio (descending) sorted_items = sorted(items, key=lambda x: x['value']/x['weight'], reverse=True) print("Items sorted by ratio (descending):") for item in sorted_items: ratio = item['value'] / item['weight'] print(f" {item['name']}: value={item['value']}, weight={item['weight']}, ratio={ratio:.2f}") greedy_selection = [] greedy_weight = 0 greedy_value = 0 remaining_capacity = capacity print(f"\nGreedy execution (capacity = {capacity}):") for item in sorted_items: if item['weight'] <= remaining_capacity: greedy_selection.append(item['name']) greedy_weight += item['weight'] greedy_value += item['value'] remaining_capacity -= item['weight'] print(f" TAKE {item['name']}: weight={item['weight']}, value={item['value']}, remaining={remaining_capacity}") else: print(f" SKIP {item['name']}: weight={item['weight']} > remaining={remaining_capacity}") print(f"\nGreedy result: {greedy_selection}, total value = {greedy_value}\n") # Step 3: Alternative Solution Feasibility print("=== Step 3: Alternative Solution Verification ===") alternative_selection = ['B', 'C'] alternative_items = [item for item in items if item['name'] in alternative_selection] alternative_weight = sum(item['weight'] for item in alternative_items) alternative_value = sum(item['value'] for item in alternative_items) print(f"Alternative selection: {alternative_selection}") print(f" Total weight: {alternative_weight}") print(f" Total value: {alternative_value}") assert alternative_weight <= capacity, "Alternative exceeds capacity!" print(f"✓ Alternative weight {alternative_weight} ≤ capacity {capacity}\n") # Step 4: Strict Improvement Check print("=== Step 4: Improvement Verification ===") print(f"Greedy value: {greedy_value}") print(f"Alternative value: {alternative_value}") if alternative_value > greedy_value: improvement = alternative_value - greedy_value pct = (alternative_value / greedy_value - 1) * 100 print(f"✓ Alternative is STRICTLY BETTER by {improvement} ({pct:.1f}% improvement)") print("\n=== COUNTEREXAMPLE VERIFIED ===") else: print("✗ Not a valid counterexample") if __name__ == "__main__": verify_knapsack_counterexample()What if you've tried multiple counterexample patterns and none of them work? This can indicate one of several situations:
The Bridge to Proofs:
Failing to find a counterexample after substantial effort is evidence (but not proof) that greedy might work. At this point, shift your approach:
State the greedy choice property precisely — What exactly is the claim that the first greedy choice is safe?
Attempt an exchange argument — If we had an optimal solution that didn't start with the greedy choice, can we exchange to get one that does without loss of quality?
Verify optimal substructure — After making the greedy choice, is the remaining problem a smaller instance of the same problem?
If you can establish these, the greedy algorithm is correct. If you find a gap in the proof logic, that gap often points directly to a counterexample structure.
Counterexample construction and proof construction are dual activities. Every failed proof attempt suggests where to look for counterexamples. Every failed counterexample attempt suggests that the proof might succeed. Alternate between the two approaches to maximize insight.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
import randomfrom itertools import combinations def greedy_knapsack(items, capacity): """Greedy by value/weight ratio.""" sorted_items = sorted(items, key=lambda x: x[1]/x[0], reverse=True) total_value = 0 total_weight = 0 for weight, value in sorted_items: if total_weight + weight <= capacity: total_value += value total_weight += weight return total_value def optimal_knapsack(items, capacity): """Brute-force optimal (exponential, for small inputs only).""" best = 0 for r in range(len(items) + 1): for subset in combinations(items, r): total_weight = sum(item[0] for item in subset) total_value = sum(item[1] for item in subset) if total_weight <= capacity and total_value > best: best = total_value return best def search_for_counterexamples(num_trials=10000, max_items=5, max_weight=20, max_value=100, max_capacity=50): """ Randomly generate inputs and check if greedy ever fails. """ counterexamples_found = [] for trial in range(num_trials): # Generate random items n_items = random.randint(2, max_items) items = [(random.randint(1, max_weight), random.randint(1, max_value)) for _ in range(n_items)] capacity = random.randint(1, max_capacity) greedy_result = greedy_knapsack(items, capacity) optimal_result = optimal_knapsack(items, capacity) if greedy_result < optimal_result: counterexamples_found.append({ 'items': items, 'capacity': capacity, 'greedy': greedy_result, 'optimal': optimal_result, 'gap': optimal_result - greedy_result }) print(f"Searched {num_trials} random inputs") print(f"Found {len(counterexamples_found)} counterexamples") if counterexamples_found: # Show the smallest counterexample smallest = min(counterexamples_found, key=lambda x: len(x['items'])) print(f"\nSmallest counterexample:") print(f" Items (weight, value): {smallest['items']}") print(f" Capacity: {smallest['capacity']}") print(f" Greedy value: {smallest['greedy']}") print(f" Optimal value: {smallest['optimal']}") # Show the largest gap worst = max(counterexamples_found, key=lambda x: x['gap']) print(f"\nLargest gap counterexample:") print(f" Items: {worst['items']}") print(f" Capacity: {worst['capacity']}") print(f" Greedy: {worst['greedy']}, Optimal: {worst['optimal']}, Gap: {worst['gap']}") if __name__ == "__main__": search_for_counterexamples()In technical interviews, counterexample construction is both a debugging tool and a demonstration of algorithmic sophistication. Interviewers are often more impressed by a candidate who can articulate why their initial greedy approach fails than by one who stumbles onto the right algorithm with no understanding of alternatives.
Scenario: You're solving a problem and propose a greedy approach. The interviewer asks: "Are you sure that works?"
This is your cue to do one of two things:
Strong candidates don't just solve problems—they demonstrate understanding of WHY their solution works. Finding a counterexample to disprove a wrong approach is as valuable as finding the right approach. It shows you can self-correct and have deep algorithmic intuition.
Common Interview Problem Types Where Greedy Fails (and candidates often try it):
| Problem | Common (Wrong) Greedy | Why It Fails | Correct Approach |
|---|---|---|---|
| Coin Change | Largest coin first | Non-canonical coins | DP |
| 0/1 Knapsack | Best ratio first | Discrete choices | DP |
| Longest Increasing Subsequence | Greedy extend | Can't undo past choices | DP |
| Edit Distance | Greedy match | Needs optimal alignment | DP |
| Partition Equal Subset | Greedy fit | Subset sum is NP-hard | DP |
| Word Break | Greedy longest match | Can block valid breaks | DP/Backtracking |
Knowing these patterns helps you avoid greedy traps and quickly pivot to correct approaches.
Beyond manual counterexample construction, experienced algorithm designers use systematic techniques to generate adversarial inputs. These techniques are particularly useful when testing algorithm implementations for edge cases.
Technique 1: Boundary Value Analysis
Greedy algorithms often have decision boundaries. Construct inputs that sit exactly on these boundaries:
Technique 2: Extremal Inputs
Push parameters to extremes:
Technique 3: Parameterized Construction
Create a family of inputs controlled by a parameter k:
For k from 2 to 10:
Create k small items that collectively beat one large item
Run greedy and check if it picks the large item wrongly
This helps find the minimum k where greedy fails.
Technique 4: Inverse Engineering
Start with the solution and work backward:
Example: For interval scheduling, if optimal selects intervals {A, B, C}:
In competitive programming and software testing, 'stress testing' generates thousands of random inputs and compares the output of a suspected-correct algorithm against a known-correct (but slower) algorithm. Any discrepancy immediately yields a counterexample for debugging.
Constructing counterexamples is a fundamental skill that separates novice problem-solvers from experts. We've covered the complete methodology:
You now possess the tools to systematically break incorrect greedy algorithms. The next page will cover the complementary skill: recognizing when greedy IS applicable and how to identify problems with the greedy choice property.