A greedy algorithm makes the locally optimal choice at each step, hoping this leads to a globally Optimal solution. Unlike dynamic programming, greedy algorithms do not consider all possible Subproblems — they commit to a choice and never reconsider.
Signal Try Greedy First? Problem has a matroid structure Yes Activity/resource scheduling with ordering Yes Huffman-like optimal prefix coding Yes Fractional version of a knapsack problem Yes MST or shortest path on non-negative weights Yes 0/1 knapsack, partition, edit distance No (use DP) TSP No (NP-hard) Problem requires “try all possibilities” to verify correctness Probably No
The exchange argument is the primary proof technique for greedy correctness. The idea: assume an Optimal solution differs from the greedy solution, then show we can exchange some element of the Optimal solution with the greedy choice without making the solution worse.
Let G G G be the greedy solution and O O O be an optimal solution Find the first point where G G G and O O O differ Show that replacing the optimal”s choice with the greedy’s choice produces a solution O ′ O' O ′ that is at least as good as O O O Conclude that there exists an optimal solution that agrees with the greedy at this step By induction, the greedy solution is optimal Given n n n activities with start times s i s_i s i and finish times f i f_i f i Select the maximum number of Non-overlapping activities.
Greedy : always pick the activity with the earliest finish time.
def activity_selection ( activities ):
Maximum number of non-overlapping activities.
Greedy: sort by finish time, pick earliest finishing.
Time: O(n log n) for sorting
Space: O(1) (excluding input)
sorted_activities = sorted (activities, key =lambda x : x[ 1 ])
last_finish = float ( ' -inf ' )
for start, finish in sorted_activities:
Exchange argument proof:
Let G = { g 1 , g 2 , … } G = \{g_1, g_2, \ldots\} G = { g 1 , g 2 , … } be the greedy solution and O = { o 1 , o 2 , … } O = \{o_1, o_2, \ldots\} O = { o 1 , o 2 , … } be an optimal Solution, both sorted by finish time. g 1 g_1 g 1 has the earliest finish time of all activities. Since o 1 o_1 o 1 also finishes before o 2 , o 3 , … o_2, o_3, \ldots o 2 , o 3 , … We have f ( g 1 ) ≤ f ( o 1 ) f(g_1) \le f(o_1) f ( g 1 ) ≤ f ( o 1 ) . Replacing o 1 o_1 o 1 with g 1 g_1 g 1 in O O O gives a valid solution (since g 1 g_1 g 1 finishes no later than o 1 o_1 o 1 It does not overlap With o 2 o_2 o 2 ). The new solution has the same size as O O O and starts with g 1 g_1 g 1 . By induction, ∣ G ∣ = ∣ O ∣ |G| = |O| ∣ G ∣ = ∣ O ∣ .
Start time and shortest duration do NOT work. The key insight is that picking the activity that Finishes earliest leaves the maximum remaining time for other activities.Huffman coding constructs an optimal prefix-free code for a set of symbols with given frequencies. It produces a binary tree where more frequent symbols have shorter codes.
Create a leaf node for each symbol with its frequency Repeatedly merge the two nodes with the smallest frequencies The merged node’s frequency is the sum of its children’s frequencies Continue until one tree remains def huffman ( frequencies ):
Build Huffman codes from symbol frequencies.
Time: O(n log n) where n = number of symbols
Returns: dict mapping symbol -> code string
heap = [(freq, i, sym) for i, (sym, freq) in enumerate (frequencies.items())]
f1, _, n1 = heapq.heappop(heap)
f2, _, n2 = heapq.heappop(heap)
children[(merged, counter)] = (n1, n2)
heapq.heappush(heap, (merged, counter, (merged, counter)))
def assign_codes ( node , code ):
if isinstance (node, str ):
codes[node] = code if code else ' 0 '
left, right = children[node]
assign_codes(left, code + ' 0 ' )
assign_codes(right, code + ' 1 ' )
if isinstance (root, str ):
Claim : Huffman’s algorithm produces a prefix code with minimum expected length.
Proof sketch :
Lemma 1 : In an optimal prefix code, the two least frequent symbols are siblings at the deepest level. If they were not, swapping a more frequent symbol deeper would not increase the expected length.
Lemma 2 : The Huffman merge step preserves optimality. If we have an optimal code for n − 1 n-1 n − 1 symbols (where the two least frequent symbols are merged), we can expand the merged symbol back into two siblings to get an optimal code for n n n symbols.
By induction : The algorithm produces optimal codes at every step.
L = \sum_{i=1}^{n} f_i \cdot \mathrm{len(c_i)
For a source with entropy H = − ∑ f i log 2 f i H = -\sum f_i \log_2 f_i H = − ∑ f i log 2 f i Huffman coding satisfies H ≤ L < H + 1 H \le L \lt H + 1 H ≤ L < H + 1 (one bit per symbol worse than the theoretical minimum).
Unlike 0/1 knapsack, the fractional version allows taking fractions of items. Greedy works: sort by Value-to-weight ratio and take as much as possible of the highest-ratio items.
def fractional_knapsack ( weights , values , capacity ):
Fractional knapsack — can take fractions of items.
key =lambda x : x[ 0 ] / x[ 1 ],
total_value += take * (v / w)
Consider items with (value, weight): (60, 10)``(100, 20)``(120, 30) with capacity 50.
Greedy by ratio: take item 1 (60/10 = 6), item 2 (100/20 = 5), item 3 (120/30 = 4). Total: 60 + 100 = 160, weight 30. Cannot add item 3 (weight 30 > remaining 20). Optimal: items 2 and 3. Total: 100 + 120 = 220, weight 50. The greedy choice of the highest-ratio item excludes the optimal combination. This is because 0/1 Knapsack lacks the matroid structure that fractional knapsack has.
When each interval has a weight and we want to maximise total weight (not count), greedy by earliest Finish time does not work. Use DP instead.
def weighted_interval_scheduling ( intervals ):
Maximum weight set of non-overlapping intervals.
intervals.sort( key =lambda x : x[ 1 ])
starts = [iv[ 0 ] for iv in intervals]
def latest_non_overlapping ( j ):
i = bisect.bisect_right(starts, intervals[j][ 0 ]) - 1
for j in range ( 1 , n + 1 ):
include = intervals[j - 1 ][ 2 ] + dp[latest_non_overlapping(j - 1 ) + 1 ]
dp[j] = max (include, exclude)
Given n n n intervals, find the minimum number of rooms needed to schedule all meetings without Overlap.
def min_meeting_rooms ( intervals ):
Minimum number of rooms for all meetings.
Greedy: sort start times, use min-heap for end times.
intervals.sort( key =lambda x : x[ 0 ])
for start, end in intervals:
if heap and heap[ 0 ] <= start:
heapq.heappush(heap, end)
Repeatedly add the cheapest edge that does not create a cycle. This produces a minimum spanning Tree.
self .parent = list ( range (n))
self .parent[x] = self .find( self .parent[x])
px, py = self .find(x), self .find(y)
if self .rank[px] < self .rank[py]:
if self .rank[px] == self .rank[py]:
Minimum spanning tree using Kruskal's algorithm.
Time: O(E log E) for sorting
edges.sort( key =lambda x : x[ 2 ])
Grow the MST from an arbitrary vertex, always adding the cheapest edge connecting the tree to a Non-tree vertex.
Minimum spanning tree using Prim's algorithm.
Time: O((V + E) log V) with binary heap
weight, u, parent = heapq.heappop(min_heap)
mst.append((parent, u, weight))
heapq.heappush(min_heap, (w, v, u))
Dijkstra’s algorithm is greedy: it always processes the vertex with the smallest tentative distance. The greedy choice is safe because with non-negative weights, the shortest path to any vertex through Already-processed vertices cannot be improved by going through unprocessed vertices.
def dijkstra ( n , graph , source ):
Shortest paths from source using Dijkstra.
Requires non-negative edge weights.
dist = [ float ( ' inf ' )] * n
if dist[u] + w < dist[v]:
heapq.heappush(pq, (dist[v], v))
Unprocessed vertex may exist. Use Bellman-Ford ($O(VE)$) for graphs with negative weights but no Negative cycles.A matroid is a combinatorial structure that captures the notion of “independence.” Greedy algorithms Are optimal on matroids.
A matroid M = ( S , I ) M = (S, \mathcal{I}) M = ( S , I ) consists of a finite set S S S and a collection I \mathcal{I} I of Independent subsets of S S S satisfying:
Hereditary property : if A ∈ I A \in \mathcal{I} A ∈ I and B ⊆ A B \subseteq A B ⊆ A Then B ∈ I B \in \mathcal{I} B ∈ I Exchange property : if A , B ∈ I A, B \in \mathcal{I} A , B ∈ I and ∣ A ∣ < ∣ B ∣ |A| \lt |B| ∣ A ∣ < ∣ B ∣ Then there exists x ∈ B ∖ A x \in B \setminus A x ∈ B ∖ A such that A ∪ { x } ∈ I A \cup \{x\} \in \mathcal{I} A ∪ { x } ∈ I Theorem : The greedy algorithm (sort elements by weight, add each element if the result remains Independent) finds the maximum-weight independent set in any matroid.
Matroid Set S S S Independent Sets I \mathcal{I} I Greedy Problem Graphic matroid Edges of a graph Acyclic subsets (forests) MST (Kruskal) Partition matroid Elements At most one from each partition Assignment Linear matroid Vectors Linearly independent sets Max weight basis Uniform matroid Elements Subsets of size ≤ k \le k ≤ k Top-k selection Transversal matroid Elements System of distinct representatives Bipartite matching
graph TD
MATROID["Matroid M = (S, I)"]
MATROID --> H["Hereditary: subset of independent is independent"]
MATROID --> E["Exchange: can extend smaller independent set from larger"]
MATROID --> G["Greedy is optimal: max-weight independent set"] The independent sets of the 0/1 knapsack (sets whose total weight does not exceed capacity) do not Satisfy the exchange property. Consider capacity 10, items of weights {6, 6, 5}. Sets {6} and {5} Are independent, but neither can be extended by the other to remain within capacity 10. This is why Greedy fails for 0/1 knapsack.
Minimise average completion time by processing jobs in order of increasing processing time.
def spt_scheduling ( jobs ):
Shortest Processing Time scheduling.
Minimises mean completion time.
total_completion += completion_time
return total_completion / len (jobs)
Schedule jobs with deadlines to maximise the number of on-time completions.
def earliest_deadline_first ( jobs ):
Schedule jobs to maximise number completed before deadline.
Greedy: sort by deadline, process in order.
jobs.sort( key =lambda x : x[ 1 ])
for duration, deadline in jobs:
if current_time <= deadline:
def weighted_job_scheduling ( jobs ):
Maximise profit with deadlines (each job takes 1 unit).
Greedy: sort by profit descending, schedule at latest available slot.
Time: O(n log n) with union-find optimisation
jobs.sort( key =lambda x : x[ 1 ], reverse = True )
max_deadline = max (j[ 2 ] for j in jobs) if jobs else 0
slots = [ - 1 ] * (max_deadline + 1 )
if slots[deadline] == - 1 :
slots[deadline] = find_slot(slots[deadline] - 1 )
for profit, _, deadline in jobs:
slot = find_slot(deadline)
Greedy coin change (always take the largest coin possible) works for certain coin systems called canonical systems .
def greedy_coin_change ( amount , coins ):
Greedy coin change — optimal for canonical coin systems.
Time: O(amount / min_coin) = O(amount)
return count if amount == 0 else - 1
Coins {1, 3, 4}Amount 6:
Greedy: 4 + 1 + 1 = 3 coins Optimal: 3 + 3 = 2 coins The coin system {1, 3, 4} is not canonical. For non-canonical systems, use DP.
def dp_coin_change ( amount , coins ):
DP coin change — works for any coin system.
Time: O(amount * len(coins))
dp = [ float ( ' inf ' )] * (amount + 1 )
for a in range ( 1 , amount + 1 ):
dp[a] = min (dp[a], dp[a - coin] + 1 )
return dp[amount] if dp[amount] != float ( ' inf ' ) else - 1
Given a universe U U U and a collection of subsets S 1 , S 2 , … , S m S_1, S_2, \ldots, S_m S 1 , S 2 , … , S m Find the minimum number of Subsets whose union is U U U . This is NP-hard, but a greedy algorithm gives a ( ln n + 1 ) (\ln n + 1) ( ln n + 1 ) -approximation.
def greedy_set_cover ( universe , subsets ):
Greedy set cover — (ln n + 1)-approximation.
Time: O(|U| * m^2) naive, O(|U| * m) with efficient tracking
uncovered = set (universe)
best_subset = max (subsets, key =lambda s : len (s & uncovered))
cover.append(best_subset)
subsets.remove(best_subset)
Polynomial-time algorithms (assuming P != NP). The $(\ln n + 1)$ bound is tight — there exist Instances where greedy achieves no better than this ratio.Express a fraction a / b a/b a / b as a sum of distinct unit fractions (fractions with numerator 1). The Greedy algorithm always works for Egyptian fractions.
def egyptian_fraction ( a , b ):
Greedy Egyptian fraction decomposition.
Always terminates (Fibonacci-Sylvester algorithm).
Time: O(log b) iterations
When you encounter a problem that could be solved by either greedy or DP:
If the independent sets form a matroid, greedy is optimal. Check the hereditary and exchange Properties.
Assume greedy is not optimal. Can you construct a counterexample? If you cannot construct one after Trying several cases, try to prove it with an exchange argument. If the proof works, greedy is Correct.
Greedy works Greedy fails (use DP) Fractional knapsack 0/1 knapsack Activity selection (max count) Weighted activity selection Huffman coding Optimal BST MST (Kruskal/Prim) Steiner tree Dijkstra (non-negative) Bellman-Ford (negative weights) Earliest deadline first Weighted interval scheduling Canonical coin change General coin change Set cover approximation Exact set cover
Test your greedy solution on small inputs and compare with brute force. If they disagree, greedy is Wrong. If they agree on many small inputs, greedy is likely correct (but not proven).
The most dangerous pitfall: writing a greedy solution that looks correct but produces wrong answers On some inputs. Always either prove correctness with an exchange argument or test against brute Force on small inputs. Greedy solutions that are not provably correct are essentially guesses.
Activity selection by earliest start time is wrong. Shortest job first for weighted completion time Is wrong. The greedy criterion must be chosen carefully and justified. When unsure, try all Reasonable sorting criteria on small examples.
Sometimes greedy works but with a different criterion. For activity selection, earliest finish time Works but earliest start time and shortest duration do not. For coin change, largest-first works for Canonical systems. Always consider multiple greedy strategies before concluding that greedy does not Apply.
Fractional knapsack admits a greedy solution (sort by ratio), but 0/1 knapsack does not. The Difference is that in the fractional version, you can take a fraction of an item, which preserves The matroid structure. Read the problem statement carefully to determine which version applies.
Dijkstra’s greedy choice (process the vertex with the smallest distance) is safe only with Non-negative weights. With negative weights, a shorter path through an unprocessed vertex may exist. Use Bellman-Ford (O ( V E ) O(VE) O ( V E ) ) instead, or add a constant to all weights (which does NOT work — it Changes relative path costs).
The greedy set cover algorithm has an approximation ratio of ln n + O ( 1 ) \ln n + O(1) ln n + O ( 1 ) Not 2. A common mistake Is to think greedy gives a constant-factor approximation. For some inputs, greedy is off by a factor Of ln n \ln n ln n .
When two elements have the same greedy value (e.g., same finish time), the tie-breaking rule can Matter. In activity selection, ties can be broken arbitrarily. In Huffman coding, ties in frequency Should be broken consistently. In MST algorithms, ties in edge weight should be handled carefully to Avoid creating cycles.
Online problems (where decisions must be made without knowledge of future inputs) often have no Optimal greedy strategy. For example, the online paging problem has a competitive ratio of k k k for LRU (greedy by recency), while the optimal offline algorithm (Belady’s) is k / ( k − h + 1 ) k/(k-h+1) k / ( k − h + 1 ) competitive. Distinguish between online and offline versions of problems.
The minimum bottleneck spanning tree minimises the maximum edge weight in the tree. Any MST is also An MBST, so Kruskal’s or Prim’s algorithm directly solves this problem.
def min_bottleneck_spanning_tree ( n , edges ):
Find the minimum bottleneck spanning tree.
Time: O(E log E) via Kruskal
mst, _ = kruskal(n, edges)
bottleneck = max (w for _, _, w in mst)
Find an MST where no vertex has degree exceeding d d d . This is NP-hard , but a greedy Approach works well as a heuristic: run Kruskal but skip any edge that would cause a vertex to Exceed degree d d d .
Given strings s s s and t t t Find the minimum window in s s s that contains all characters of t t t .
from collections import Counter
def min_window_substring ( s , t ):
Minimum window in s containing all characters of t.
result = ( 0 , float ( ' inf ' ))
for right, ch in enumerate (s):
if right - left < result[ 1 ] - result[ 0 ]:
return "" if result[ 1 ] == float ( ' inf ' ) else s[result[ 0 ] : result[ 1 ] + 1 ]
Rearrange characters so that no two adjacent characters are the same.
def reorganise_string ( s ):
Rearrange string so no two adjacent chars are the same.
Greedy: always place the most frequent remaining character.
Time: O(n log ALPHABET_SIZE) = O(n)
max_freq = max (freq.values())
if max_freq > ( len (s) + 1 ) // 2 :
heap = [( - count, ch) for ch, count in freq.items()]
prev_count, prev_ch = 0 , ''
count, ch = heapq.heappop(heap)
heapq.heappush(heap, (prev_count, prev_ch))
prev_count, prev_ch = count, ch
Find the minimum number of jumps to reach the last index of an array where arr[i] is the maximum Jump length from position i i i .
Minimum jumps to reach the last index.
Greedy: at each step, jump to the position that maximises reach.
for i in range ( len (nums) - 1 ):
farthest = max (farthest, i + nums[i])
Find the starting gas station index from which you can travel around the circuit once.
def gas_station ( gas , cost ):
Find starting gas station to complete circuit.
Greedy: if total gas >= total cost, a solution exists.
The starting point is where the running sum is minimum.
for i in range ( len (gas)):
total_tank += gas[i] - cost[i]
curr_tank += gas[i] - cost[i]
return start if total_tank >= 0 else - 1
Given n n n sorted files of sizes s 1 , s 2 , … , s n s_1, s_2, \ldots, s_n s 1 , s 2 , … , s n Merge them into one sorted file with Minimum total comparisons. This is identical to Huffman coding.
def optimal_merge ( files ):
Minimum total comparisons to merge sorted files.
Same as Huffman coding — merge two smallest each time.
heapq.heappush(files, merged)
Graham scan for convex hull.
return (a[ 0 ] - o[ 0 ]) * (b[ 1 ] - o[ 1 ]) - (a[ 1 ] - o[ 1 ]) * (b[ 0 ] - o[ 0 ])
points = sorted ( set (points))
while len (lower) >= 2 and cross(lower[ - 2 ], lower[ - 1 ], p) <= 0 :
for p in reversed (points):
while len (upper) >= 2 and cross(upper[ - 2 ], upper[ - 1 ], p) <= 0 :
return lower[ : - 1 ] + upper[ : - 1 ]
Assign the minimum number of resources (colours) to intervals so that overlapping intervals have Different colours. This is the interval graph colouring problem.
def interval_colouring ( intervals ):
Minimum colours for interval graph (chromatic number).
Greedy: sort by start time, assign smallest available colour.
intervals = sorted (intervals, key =lambda x : x[ 0 ])
for i, (start, end) in enumerate (intervals):
if end_times and end_times[ 0 ] <= start:
colour = heapq.heappop(end_times)
colour = len (end_times) + 1
heapq.heappush(end_times, (end, colour))
return colours, len (end_times)
Technique When to Use Key Idea Exchange argument Scheduling, MST, Huffman, matroids Swap optimal’s choice with greedy’s Greedy stays ahead Simple greedy with clear ordering Show greedy’s partial solution is never worse Cut-and-paste Partitioning problems Cut one solution, paste into another Induction Any greedy with natural ordering Prove step k k k implies step k + 1 k+1 k + 1 Matroid theorem When problem has matroid structure Greedy is optimal on matroids Lower bound matching Approximation algorithms Show greedy achieves a known lower bound
This topic covers the core concepts of greedy algorithms, including underlying theory, practical implementation, and key applications.
Key concepts include:
Big O notation and complexity analysis searching algorithms (binary, linear) sorting algorithms (bubble, merge, quick) graph algorithms (Dijkstra, BFS, DFS) dynamic programming Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Dynamic Programming Introduction — Greedy and DP are contrasting paradigms; understanding when each applies is fundamental to algorithm design.Graph Algorithms — Dijkstra’s algorithm is a classic greedy algorithm applied to shortest path problems.Binary Search Trees — Huffman coding and optimal BSTs use greedy strategies for efficient data encoding and retrieval.Advanced Graph Algorithms — Kruskal’s and Prim’s MST algorithms are greedy algorithms applied to graph structures.Greedy algorithms follow a simple philosophy: make the locally optimal choice at each step and hope it leads to the globally optimal solution. This works when the problem has a specific mathematical structure — in most cases a matroid or an exchange property that guarantees local choices don’t lock you out of the global optimum. For example, in activity selection, picking the activity that finishes earliest always leaves the most room for future activities. In Huffman coding, merging the two smallest frequencies always produces an optimal prefix code. The beauty of greedy is its simplicity: sort, iterate, pick the best available option.
The critical question is: when does greedy fail? It fails when a locally optimal choice can lead you into a dead end that a globally optimal solution would have avoided. The 0/1 knapsack is the classic example — greedily picking the highest value-per-weight item can leave you unable to fit another item that would have been better overall. Fractional knapsack works greedily because you can always take a piece of the next item. The distinction comes down to whether the problem has the matroid exchange property: can you always extend a smaller feasible solution using elements from a larger one without breaking feasibility?
The decision framework is practical: first check if the problem has matroid structure (greedy is optimal). If not, try an exchange argument — assume greedy is wrong and try to construct a counterexample. If you can’t find one, try to prove greedy works by showing that swapping greedy’s choice with the optimal choice never improves the result. If that proof fails, fall back to dynamic programming. For approximation problems like set cover, greedy gives a provably good approximation (within ln n of optimal) even when the exact problem is NP-hard.