Loading content...
Imagine a chess player who never thinks more than one move ahead and never takes back a piece once placed. At first glance, this seems like a recipe for disaster. Yet for certain board configurations—special games designed with the right structure—such a player can still win optimally.
This is the essence of greedy algorithms: decision-making under severe self-imposed constraints. A greedy algorithm:
These constraints seem crippling. How can you solve optimization problems without exploring alternatives or planning ahead? The answer lies in problem structure. When problems possess certain mathematical properties, these constraints don't handicap us—they free us from unnecessary computation.
By the end of this page, you will understand why greedy algorithms forbid backtracking and lookahead, how these constraints enable efficiency, when these constraints cause failure, and how to recognize problem structures compatible with greedy constraints.
Backtracking in algorithm design refers to the practice of undoing previous decisions when they lead to dead ends or suboptimal outcomes. It's a fundamental technique in exhaustive search, constraint satisfaction, and many optimization algorithms.
Greedy algorithms completely forbid backtracking. Once a choice is made, it becomes part of the solution permanently. There is no mechanism to "undo" or "retry."
What this means in practice:
Why would anyone design an algorithm this way?
The no-backtracking constraint delivers massive efficiency gains:
1. Time Complexity:
2. Space Complexity:
3. Simplicity:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
"""Comparison: Backtracking vs Greedy approaches to subset selection. Problem: Select maximum weight subset with total ≤ capacity.""" from typing import Optional # Approach 1: BACKTRACKING# Explores all possibilities, backtracks from dead ends def backtracking_subset( weights: list[int], capacity: int) -> tuple[int, list[int]]: """ Find maximum weight subset with total ≤ capacity. Uses backtracking to explore all 2^n subsets. Time: O(2^n) - exponential Space: O(n) - recursion stack """ best_weight = 0 best_subset: list[int] = [] def backtrack(index: int, current_weight: int, current_subset: list[int]): nonlocal best_weight, best_subset # Update best if current is better if current_weight > best_weight: best_weight = current_weight best_subset = current_subset.copy() # Try including remaining elements for i in range(index, len(weights)): if current_weight + weights[i] <= capacity: # CHOICE: Include this element current_subset.append(i) current_weight += weights[i] # EXPLORE: Recurse backtrack(i + 1, current_weight, current_subset) # UNCHOOSE: Backtrack - undo the decision current_subset.pop() current_weight -= weights[i] backtrack(0, 0, []) return best_weight, best_subset # Approach 2: GREEDY# Makes immediate choices, never reconsiders def greedy_subset( weights: list[int], capacity: int) -> tuple[int, list[int]]: """ Greedy approach: take largest weights that fit. Time: O(n log n) - dominated by sort Space: O(n) - for sorted indices NOTE: This is NOT optimal for this problem! But it demonstrates the greedy structure. """ # Sort indices by weight (descending) sorted_indices = sorted(range(len(weights)), key=lambda i: weights[i], reverse=True) total_weight = 0 selected = [] for i in sorted_indices: if total_weight + weights[i] <= capacity: # COMMIT: Include this element PERMANENTLY selected.append(i) total_weight += weights[i] # NO BACKTRACKING: This decision is final return total_weight, selected # Comparisonweights = [8, 4, 5, 2, 3]capacity = 10 print("Weights:", weights)print("Capacity:", capacity)print() bt_weight, bt_subset = backtracking_subset(weights, capacity)print(f"Backtracking: weight={bt_weight}, indices={bt_subset}")print(f" Items: {[weights[i] for i in bt_subset]}") gr_weight, gr_subset = greedy_subset(weights, capacity)print(f"Greedy: weight={gr_weight}, indices={gr_subset}")print(f" Items: {[weights[i] for i in gr_subset]}") # Backtracking finds optimal: 4+5=9 or 8+2=10 or 4+3+2=9...# Actually optimal: 8+2=10 or 5+3+2=10# Greedy: 8 first, then 2 = 10 (happens to be optimal here) # Counterexample where greedy fails:weights2 = [6, 5, 5]capacity2 = 10print()print("Counterexample:")print("Weights:", weights2, "Capacity:", capacity2)bt_weight2, bt_subset2 = backtracking_subset(weights2, capacity2)gr_weight2, gr_subset2 = greedy_subset(weights2, capacity2)print(f"Backtracking: {bt_weight2} using {[weights2[i] for i in bt_subset2]}")print(f"Greedy: {gr_weight2} using {[weights2[i] for i in gr_subset2]}")# Backtracking: 5+5=10# Greedy: 6 first, can't fit any 5, total=6# Greedy FAILS because it can't undo taking 6As the counterexample shows, no backtracking means greedy can't recover from 'locally good, globally bad' choices. When the greedy algorithm takes the 6, it cannot undo this when it realizes two 5s would be better. This is the fundamental trade-off: blazing speed at the cost of potential suboptimality.
Looking ahead refers to analyzing future states or consequences before making a decision. Game-playing algorithms look ahead several moves; dynamic programming considers all paths; branch-and-bound prunes based on projected outcomes.
Greedy algorithms do not look ahead. Each decision is made based solely on:
The algorithm has no concept of "if I take X now, then Y might happen later." It sees only the immediate choice.
What no-looking-ahead means concretely:
1. No Simulation of Future States: The algorithm doesn't compute "what happens if I choose A vs B" down the line. It computes "which is better right now" and chooses.
2. No Dependency on Future Input: In online/streaming settings, future input is literally unknown. Greedy is natural here—you can't look ahead to what hasn't arrived.
3. No Multi-Step Planning: A chess engine might think "if I move queen here, opponent moves bishop there, I respond with rook..." Greedy thinks only "which move looks best right now."
4. No Regret Considerations: The algorithm doesn't ask "will I regret this choice later?" It asks only "is this the best choice now?"
| Paradigm | Decision Horizon | Information Used | Typical Application |
|---|---|---|---|
| Greedy | Current step only | Current state + candidates | Scheduling, MST, Huffman |
| DP (Bottom-Up) | All future subproblems | All smaller subproblems | Knapsack, Edit Distance |
| DP (Memoization) | Needed future states | Computed on demand | Recursive optimization |
| Backtracking | Until dead end / solution | Explored paths | CSP, Puzzles, N-Queens |
| Minimax | K moves ahead | Game tree to depth K | Chess, Go, adversarial |
| Branch & Bound | Pruned subtrees | Bounds on unexplored | Integer programming |
Zero lookahead means O(1) time per decision (after preprocessing). If you can make n decisions independently in O(1) each, your algorithm is O(n)—as fast as reading the input. This is why greedy algorithms are often asymptotically optimal in time complexity.
Together, no-backtracking and no-looking-ahead create what we might call a myopic lens—the algorithm sees only the immediate situation. This myopia has profound implications for algorithm design and correctness.
The Myopic Success Condition:
Greedy succeeds when the problem structure guarantees that myopic decisions are as good as omniscient decisions. Specifically:
If, at every decision point, the locally optimal choice is also globally justifiable—meaning no omniscient algorithm could do better given the same past—then greedy is optimal.
This is precisely what the greedy choice property formalizes: there exists an optimal solution that includes the greedy choice. If such a solution exists, myopia costs us nothing.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
"""Illustration: When myopia succeeds vs when it fails. We compare greedy's myopic decision to omniscient hindsight.""" from dataclasses import dataclass @dataclassclass Activity: name: str start: int end: int def greedy_activity_selection(activities: list[Activity]) -> list[Activity]: """ Myopic selection: always take the activity that ends earliest. At each step, we see only: - Current time (last activity's end time) - Remaining candidates that start after current time - Their end times We do NOT consider: - What activities might be blocked - Whether alternative choices lead to more selections - Any future consequences """ sorted_activities = sorted(activities, key=lambda a: a.end) selected = [] current_time = float('-inf') for activity in sorted_activities: if activity.start >= current_time: # MYOPIC DECISION: This activity ends earliest among feasible. # We take it without considering alternatives. selected.append(activity) current_time = activity.end return selected def omniscient_hindsight(activities: list[Activity]) -> int: """ Omniscient view: knows all activities, computes optimal count. (Uses DP / exact algorithm) """ if not activities: return 0 sorted_activities = sorted(activities, key=lambda a: a.end) n = len(sorted_activities) # dp[i] = max activities in sorted_activities[0..i] ending with [i] # For activity selection, greedy IS optimal, so this matches. # But the computation style is "omniscient" - considers all options. # Actually, for interval scheduling, even DP reduces to greedy. # Let's show the contrast differently: # Optimal subset via exhaustive (exponential but shows full info) best_count = 0 def explore(index: int, last_end: float, count: int): nonlocal best_count if index == n: best_count = max(best_count, count) return # Option 1: Skip this activity explore(index + 1, last_end, count) # Option 2: Take this activity (if feasible) if sorted_activities[index].start >= last_end: explore(index + 1, sorted_activities[index].end, count + 1) explore(0, float('-inf'), 0) return best_count # Example where myopia succeedsactivities = [ Activity("A", 1, 3), Activity("B", 2, 5), Activity("C", 4, 7), Activity("D", 6, 9), Activity("E", 8, 10),] greedy_result = greedy_activity_selection(activities)optimal_count = omniscient_hindsight(activities) print("Activity Selection (greedy WORKS):")print(f"Activities: {[(a.name, a.start, a.end) for a in activities]}")print(f"Greedy selected: {[a.name for a in greedy_result]} (count={len(greedy_result)})")print(f"Optimal count: {optimal_count}")print(f"Match: {len(greedy_result) == optimal_count}")print() # Illustration: WHY myopia works hereprint("Why myopia succeeds for activity selection:")print("- Taking earliest-ending A at t=1-3")print("- This NEVER blocks a better choice, because:")print(" Any activity starting after 3 is still available")print(" Any activity blocked by taking A would have ended LATER")print(" We can always 'swap' a later-ending choice for A")print("- This is the greedy choice property in action!")For problems with the greedy choice property, myopia isn't a limitation—it's an efficient exploitation of problem structure. The algorithm 'knows' (via the problem's mathematical properties) that looking ahead is unnecessary. It's not ignorant; it's optimally lazy.
Understanding when greedy's constraints cause failure is as important as knowing when they enable success. Let's examine the structural properties that break greedy algorithms.
Case Study: Traveling Salesman Problem (TSP)
TSP asks for the shortest tour visiting all cities exactly once. The nearest neighbor heuristic is a natural greedy approach:
Algorithm: From current city, go to the nearest unvisited city. Repeat.
Why it fails to be optimal:
Short-term thinking creates long-term problems: Taking the nearest neighbor might lead you into a corner of the map, requiring a long journey back.
No recovery: Once you've visited cities in a certain order, you can't backtrack. A city visited "too early" is committed.
Final edge ignored: The algorithm doesn't consider that the last edge (returning to start) might be very long if you've wandered far away.
Nearest neighbor can produce tours 25% longer than optimal on typical instances, and arbitrarily bad on adversarial instances.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
"""Demonstration: Greedy nearest-neighbor for TSP can be arbitrarily bad. We construct an adversarial example where greedy produces a tourmuch longer than optimal.""" import mathfrom itertools import permutations def distance(p1: tuple[float, float], p2: tuple[float, float]) -> float: return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) def nearest_neighbor_tsp(cities: list[tuple[float, float]]) -> tuple[float, list[int]]: """ Greedy nearest neighbor TSP. At each step: - Look at unvisited cities - Go to the closest one - No consideration of future consequences """ n = len(cities) visited = [False] * n tour = [0] # Start at city 0 visited[0] = True total_distance = 0.0 for _ in range(n - 1): current = tour[-1] nearest = -1 nearest_dist = float('inf') for j in range(n): if not visited[j]: d = distance(cities[current], cities[j]) if d < nearest_dist: nearest_dist = d nearest = j tour.append(nearest) visited[nearest] = True total_distance += nearest_dist # Return to start total_distance += distance(cities[tour[-1]], cities[0]) tour.append(0) return total_distance, tour def optimal_tsp(cities: list[tuple[float, float]]) -> float: """Brute force optimal TSP (exponential, for small n only).""" n = len(cities) if n <= 1: return 0.0 best_distance = float('inf') # Try all permutations of cities 1..n-1 (city 0 is start) for perm in permutations(range(1, n)): tour = [0] + list(perm) + [0] dist = sum(distance(cities[tour[i]], cities[tour[i+1]]) for i in range(n)) best_distance = min(best_distance, dist) return best_distance # Adversarial example: cities in a "trap" configuration# Greedy gets lured away from the efficient path cities = [ (0, 0), # Start (1, 0), # Close to start - greedy takes this first (2, 0), # Next closest - greedy continues (3, 0), # Greedy keeps going... (10, 0), # Far away - but greedy committed to this direction (1.5, 3), # Should be visited early for efficient tour!] greedy_dist, greedy_tour = nearest_neighbor_tsp(cities)optimal_dist = optimal_tsp(cities) print("Adversarial TSP Example:")print(f"Cities: {cities}")print()print(f"Greedy tour: {greedy_tour}")print(f"Greedy distance: {greedy_dist:.2f}")print()print(f"Optimal distance: {optimal_dist:.2f}")print(f"Greedy is {greedy_dist/optimal_dist:.1%} of optimal")print(f"Excess: {100*(greedy_dist/optimal_dist - 1):.1f}%") # The issue: Greedy goes 0→1→2→3→4 (rightward), then must# travel all the way back left to visit (1.5, 3), then back to 0.# Optimal would weave efficiently to minimize backtracking.For TSP, nearest neighbor can produce tours O(log n) times the optimal length in the worst case. This means no matter how clever the greedy heuristic, the problem structure fundamentally defeats myopic optimization. TSP requires global planning—exactly what greedy forbids.
There's a class of problems where no-backtracking and no-looking-ahead aren't algorithmic choices—they're constraints imposed by reality. These are online and streaming problems.
Online Algorithms: Input arrives piece by piece, and decisions must be made before seeing future input. You literally cannot look ahead.
Streaming Algorithms: Data flows through in a stream, too large to store. You process each item and discard it. You literally cannot backtrack.
In these settings, greedy isn't just one option—it's often the only viable approach.
| Problem | Input Model | Decision Constraint | Greedy Approach |
|---|---|---|---|
| Ski Rental | Unknown ski days | Buy or rent before each day | Rent until cost equals purchase price |
| Paging/Caching | Unknown future requests | Evict before each miss | LRU, LFU, or similar heuristics |
| Online Bin Packing | Items arrive one by one | Place before seeing next | First Fit, Best Fit, etc. |
| Secretary Problem | Candidates interviewed sequentially | Hire/reject immediately | Reject first n/e, then take best seen |
| Load Balancing | Jobs arrive online | Assign to machine immediately | Least loaded machine (greedy) |
Competitive Analysis:
For online algorithms, we don't expect optimal solutions—the future is unknown! Instead, we measure competitive ratio: how well does our online algorithm perform compared to an optimal offline algorithm that sees all input in advance?
A c-competitive algorithm produces solutions at most c times worse than optimal:
Online Cost ≤ c × Optimal Cost
Greedy strategies often achieve good competitive ratios:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
"""The Ski Rental Problem: A canonical online problem. You're taking a ski trip of unknown length.- Renting skis costs $1 per day- Buying skis costs $B (one-time) You don't know how many days you'll ski.Decision: Each day, rent or buy?""" def offline_optimal(num_days: int, buy_cost: int) -> int: """Optimal cost IF you knew num_days in advance.""" rent_total = num_days * 1 return min(rent_total, buy_cost) def greedy_ski_rental(num_days: int, buy_cost: int) -> tuple[int, str]: """ Greedy strategy: Rent until cumulative rent equals buy cost. Then buy. This is provably 2-competitive! """ total_cost = 0 bought = False actions = [] for day in range(1, num_days + 1): if bought: actions.append(f"Day {day}: Using owned skis (free)") # No additional cost elif total_cost >= buy_cost - 1: # Next rent would exceed buy cost, so buy now total_cost += buy_cost bought = True actions.append(f"Day {day}: BUY for ${buy_cost} (cumulative: ${total_cost})") else: total_cost += 1 actions.append(f"Day {day}: Rent for $1 (cumulative: ${total_cost})") return total_cost, actions # Examplebuy_cost = 10 print("Ski Rental Problem (Buy cost = $10)")print("=" * 50) for num_days in [3, 7, 10, 15, 20]: greedy_cost, actions = greedy_ski_rental(num_days, buy_cost) optimal = offline_optimal(num_days, buy_cost) ratio = greedy_cost / optimal if optimal > 0 else 1 print(f"\n{num_days} days:") print(f" Greedy cost: ${greedy_cost}") print(f" Optimal (hindsight): ${optimal}") print(f" Competitive ratio: {ratio:.2f}") print("\n" + "=" * 50)print("Greedy is at most 2-competitive (2x optimal).")print("This is PROVABLY the best any online algorithm can do")print("without knowing the future!")For ski rental, the greedy strategy is OPTIMALLY competitive—no algorithm can do better than 2-competitive against an adversary who knows your strategy. The greedy approach isn't just simple; it's provably the best you can do given the constraints of online decision-making.
Pure greedy (zero backtracking, zero lookahead) is sometimes too restrictive. Practical algorithms often use relaxations that allow limited deviation from strict greedy principles while retaining most of the efficiency.
Example: Greedy with Local Search for TSP
2-opt improvement: Remove two edges, reconnect differently. If tour is shorter, keep the change.
Before: A--B C--D (edges A-B and C-D)
After: A--C B--D (edges A-C and B-D)
This hybrid often produces near-optimal TSP tours in practice, despite greedy's theoretical weakness for the problem.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
"""Greedy with 2-opt local search for TSP. 1. Build initial tour greedily (nearest neighbor)2. Improve with 2-opt swaps (limited backtracking)""" import math def distance(cities, i, j): return math.sqrt((cities[i][0] - cities[j][0])**2 + (cities[i][1] - cities[j][1])**2) def tour_length(cities, tour): return sum(distance(cities, tour[i], tour[i+1]) for i in range(len(tour)-1)) def greedy_nearest_neighbor(cities): """Pure greedy: nearest neighbor construction.""" n = len(cities) visited = [False] * n tour = [0] visited[0] = True for _ in range(n - 1): current = tour[-1] nearest = min((j for j in range(n) if not visited[j]), key=lambda j: distance(cities, current, j)) tour.append(nearest) visited[nearest] = True tour.append(0) # Return to start return tour def two_opt_improvement(cities, tour): """ 2-opt local search: LIMITED backtracking. We don't undo the entire greedy solution—just try local edge swaps that might improve the tour. """ n = len(tour) - 1 # tour[0] == tour[n] (start = end) improved = True while improved: improved = False for i in range(1, n - 1): for j in range(i + 1, n): # Consider reversing tour[i:j+1] # This effectively swaps edges (i-1,i) and (j,j+1) # for edges (i-1,j) and (i,j+1) old_dist = (distance(cities, tour[i-1], tour[i]) + distance(cities, tour[j], tour[j+1])) new_dist = (distance(cities, tour[i-1], tour[j]) + distance(cities, tour[i], tour[j+1])) if new_dist < old_dist - 1e-10: # Improvement found! Apply the reversal. tour[i:j+1] = reversed(tour[i:j+1]) improved = True return tour def greedy_with_local_search(cities): """Hybrid: greedy construction + local search refinement.""" tour = greedy_nearest_neighbor(cities) tour = two_opt_improvement(cities, tour) return tour # Examplecities = [ (0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (2, 1), (3, 1), (0, 2), (1, 2), (2, 2), (3, 2),] greedy_tour = greedy_nearest_neighbor(cities)hybrid_tour = greedy_with_local_search(cities) print("TSP: Greedy vs Greedy + Local Search")print(f"Cities: 4x3 grid")print()print(f"Pure Greedy tour length: {tour_length(cities, greedy_tour):.2f}")print(f"Greedy + 2-opt length: {tour_length(cities, hybrid_tour):.2f}")print(f"Improvement: {100*(1 - tour_length(cities, hybrid_tour)/tour_length(cities, greedy_tour)):.1f}%")Greedy + 2-opt remains polynomial (typically O(n² × iterations)). It's far faster than exact TSP (O(n! or O(2^n × n²))) while producing solutions typically within 2-5% of optimal for moderate-sized instances. The limited backtracking of 2-opt captures much of the value of full optimization.
Given a new problem, how do you assess whether greedy's constraints (no backtracking, no lookahead) are compatible with optimal solving? Here are structural indicators:
Ask yourself: 'If I commit to the greedy choice now, can I EVER regret it?' If the answer is provably 'no' (perhaps via exchange argument), greedy works. If you can construct a scenario where you regret the choice, greedy fails. This intuition guides formal proof development.
We've explored the fundamental constraints that define greedy algorithms. Let's consolidate:
What's Next:
We've now covered the greedy paradigm's definition, its selection mechanics, and its fundamental constraints. The next page brings everything together with intuitive examples and concrete problem walkthroughs—solidifying your understanding with hands-on illustrations of greedy thinking.
You now understand why greedy algorithms forbid backtracking and lookahead, when these constraints enable optimal solutions, and when they cause failure. Next, we'll explore concrete examples that bring greedy intuition to life.