DSA Algorithms
Greedy Algorithms
A calendar scheduler must pack the maximum non-overlapping meetings from a list of thousands. Sorting by earliest finish and scanning once is O(n log n). Without a proof, “always take the densest” is just a vibe.
Greedy needs a justified by an or argument, plus .
Proof journey
Name the local rule
Earliest finish, lightest safe edge, …
Pick a certificate
Exchange, stay-ahead, or safe cut.
Remaining instance stays well-formed.
Implement the scan
Usually sort once, then a linear pass.
What actually happens
Naive idea: try all feasible subsets (exponential), or pick whatever looks locally best without a proof (0/1 knapsack density, arbitrary coin systems).
Better idea: sort by a key that an exchange or stay-ahead argument licenses, then scan. Activity selection by earliest finish stays ahead on finish times; Kruskal adds lightest safe edges in a graphic .
The improved design still fails when the structure is not a matroid and no exchange proof exists (0/1 knapsack), when denominations are non-canonical, or when “local best” has no measurable frontier.
Invariant
After each greedy commitment, there still exists a globally optimal completion consistent with the choices so far (exchange), or greedy’s partial solution stays ahead of every competitor on the agreed measure (stay-ahead).
How to choose greedy
1. Can you prove the local rule? No certificate means try DP or search instead.
2. Sorting + one pass enough? Many true greedy solutions look like that.
3. Counterexample hunt? Always try a tiny instance that breaks the vibe before coding.
Quantitative check
Activity selection: sort O(n log n), scan O(n). At n = 10⁶ that is fine. Enumerating independent sets of intervals is exponential. Coin change with coins {1,3,4} and amount 6: greed takes 4+1+1 (3 coins); optimum is 3+3 (2 coins).
Activity Selection Visualizer
Greedy Strategy: Always pick the next non-overlapping activity that finishes earliest.
Step 0 of -1
Core principles
1. Greedy Choice Property
A global optimum can be reached by making locally optimal choices. You can make the best decision at each step without worrying about future consequences.
Example: In coin change with standard denominations, always taking the largest coin works
2. Optimal Substructure
An optimal solution contains optimal solutions to subproblems. Like DP, but greedy makes the choice before solving subproblems, not after.
Example: Shortest path in Dijkstra's algorithm builds on shortest paths to intermediate nodes
A greedy choice needs a proof certificate
Exchange argument
Take an optimal solution O. Replace its first disagreement with the greedy choice and prove feasibility and objective value do not worsen. Repeat until O becomes greedy.
Stays ahead
After every prefix of choices, define a measurable frontier and prove greedy is at least as favorable as any competitor: earlier finish, no greater cost, or no less coverage.
Cut / safe-edge proof
Show the selected local object is safe across a boundary every optimum must cross. MST cut edges and Dijkstra's minimum unsettled distance (with nonnegative weights) use this shape.
Rule of thumb: “it leaves the most room” is intuition. Name the transformed optimum, invariant, or safe cut that turns intuition into correctness.
Recognizing matroid-like structure
A powerful signal is a family of feasible sets that is hereditary (subsets remain feasible) and satisfies an exchange property (a smaller feasible set can accept some element from a larger one). For a true matroid, sorting elements by weight and greedily adding feasible ones is optimal for every weight assignment.
- • Graphic matroid → Kruskal adds the lightest edge that does not create a cycle.
- • Uniform matroid → choose the k highest-value elements.
- • Activity selection has its own earliest-finish exchange proof; do not label every hereditary system a matroid.
- • 0/1 knapsack lacks the required exchange property, so value density is not generally safe.
🔍 Did You Know? Greedy vs DP
Both greedy optimization and DP often use optimal substructure, but there's a key difference:DP compares every relevant transition for each reached state (top-down memoization may skip unreachable states), whileGreedy makes a choice first and solves one subproblem. Greedy can do less work, but only when a proof shows that the irrevocable local choice is safe. That's why proving correctness is crucial for greedy algorithms!
🎯 Classic Example: Activity Selection
Problem: Select maximum number of non-overlapping activities
Given activities with start and end times, select the maximum number that don't overlap.
Python
def activity_selection(activities):
"""
Greedy Activity Selection
Strategy: Always pick activity that finishes earliest!
This leaves maximum room for future activities.
Time: O(n log n) - sorting
Space: O(n) - preserve the caller's list
"""
if any(finish < start for start, finish in activities):
raise ValueError("finish must be at least start")
ordered = sorted(activities, key=lambda activity: (activity[1], activity[0]))
if not ordered:
return []
selected = [ordered[0]] # Pick first activity
last_finish = ordered[0][1]
# Greedy choice: pick next activity that starts after last one finishes
for start, finish in ordered[1:]:
if start >= last_finish: # No overlap
selected.append((start, finish))
last_finish = finish
return selected
# Example
activities = [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9),
(6,10), (8,11), (8,12), (2,14), (12,16)]
result = activity_selection(activities)
print(f"Selected {len(result)} activities: {result}")
# Output: Selected 4 activities: [(1, 4), (5, 7), (8, 11), (12, 16)]
# Why greedy works here:
# Choosing earliest-finishing activity leaves most room for future choices
# This is provably optimal!Java
import java.util.*;
public class ActivitySelection {
static class Activity {
int start, finish;
Activity(int start, int finish) {
this.start = start;
this.finish = finish;
}
}
/**
* Greedy Activity Selection
*
* Strategy: Always pick activity that finishes earliest!
* This leaves maximum room for future activities.
*
* Time: O(n log n) - sorting
* Space: O(n) - preserve the caller's list
*/
public static List<Activity> activitySelection(List<Activity> activities) {
for (Activity activity : activities) {
if (activity.finish < activity.start) throw new IllegalArgumentException("invalid interval");
}
if (activities.isEmpty()) return new ArrayList<>();
List<Activity> ordered = new ArrayList<>(activities);
ordered.sort(Comparator.comparingInt((Activity a) -> a.finish)
.thenComparingInt(a -> a.start));
List<Activity> selected = new ArrayList<>();
selected.add(ordered.get(0)); // Pick first activity
int lastFinish = ordered.get(0).finish;
// Greedy choice: pick next activity that starts after last one finishes
for (int i = 1; i < ordered.size(); i++) {
Activity curr = ordered.get(i);
if (curr.start >= lastFinish) { // No overlap
selected.add(curr);
lastFinish = curr.finish;
}
}
return selected;
}
public static void main(String[] args) {
List<Activity> activities = Arrays.asList(
new Activity(1, 4), new Activity(3, 5), new Activity(0, 6),
new Activity(5, 7), new Activity(3, 9), new Activity(5, 9),
new Activity(6, 10), new Activity(8, 11), new Activity(8, 12),
new Activity(2, 14), new Activity(12, 16)
);
List<Activity> result = activitySelection(activities);
System.out.println("Selected " + result.size() + " activities");
// Output: Selected 4 activities
}
}🎯 Key Insight: When Greedy Fails
Greedy doesn't always work! Classic example: Coin change with arbitrary denominations
Coins: [1, 3, 4], Target: 6
❌ Greedy: 4 + 1 + 1 = 3 coins (takes largest first)
✅ Optimal: 3 + 3 = 2 coins (DP finds this)
Always verify greedy correctness! If greedy choice doesn't guarantee global optimum, use DP instead.
Counterexample discipline
| Tempting rule | Small failure | Lesson |
|---|---|---|
| Activity: shortest duration first | A short middle interval can block one compatible interval on each side | Earliest finish, not shortest length, admits the exchange proof |
| Activity: earliest start first | One very long early activity blocks many later activities | Optimize the future frontier, not the immediate timestamp |
| 0/1 knapsack: density first | An indivisible high-density item can prevent a better combination | Density is safe only when fractions are allowed |
| Dijkstra with a negative edge | A finalized distance can later improve through that edge | The nonnegative-weight premise is part of the proof |
Tie-breaking and API semantics
Determinism
- • Prove which fields matter to optimality; then use stable secondary keys for reproducible output.
- • Define whether touching intervals are compatible (start ≥ finish) and how zero-length jobs behave.
- • In Java, use comparator methods instead of subtraction, which can overflow.
Input ownership
- • Decide whether sorting mutates the caller's collection; production APIs should document or avoid mutation.
- • Validate NaN, zero/negative weights, malformed intervals, and overflow-prone totals.
- • Return the witness plus objective when callers need to audit the choice sequence.
How to test a greedy conjecture
- 1. Brute-force tiny inputs: enumerate every feasible subset/order and compare its optimum with greedy.
- 2. Generate adversarial families: ties, nested intervals, one dominant item, equal ratios, disconnected graphs, and boundary values.
- 3. Check invariants after each choice: feasibility never breaks and the proof's “stays-ahead” measure holds.
- 4. Shrink failures: minimize a random counterexample until the missing proof premise is obvious.
- 5. Keep the oracle tests: a proof establishes correctness; differential tests protect the implementation and tie policy.
💻 More Greedy Examples
Fractional Knapsack (Greedy Works!)
Python
from fractions import Fraction
def fractional_knapsack(items, capacity):
"""
Greedy: Take items by value/weight ratio
Can take fractions of items
Time: O(n log n)
"""
if capacity < 0:
raise ValueError("capacity must be non-negative")
if any(weight <= 0 or value < 0 for weight, value in items):
raise ValueError("expected positive weights and non-negative values")
# Fraction keeps the ordering exact even when two large ratios round to
# the same binary floating-point value.
ordered = sorted(
items,
key=lambda item: (-Fraction(item[1], item[0]), item[0], item[1]),
)
total_value = 0
remaining = capacity
for weight, value in ordered:
if remaining == 0:
break
if remaining >= weight:
# Take whole item
total_value += value
remaining -= weight
else:
# Take fraction
total_value += value * (remaining / weight)
break
return total_value
items = [(10, 60), (20, 100), (30, 120)] # (weight, value)
capacity = 50
print(f"Max value: {fractional_knapsack(items, capacity)}")
# Output: Max value: 240.0Java
import java.util.*;
public class FractionalKnapsack {
static class Item {
final int weight, value;
Item(int weight, int value) {
this.weight = weight;
this.value = value;
}
}
/**
* Greedy: Take items by value/weight ratio
* Can take fractions of items
*
* Time: O(n log n)
*/
public static double fractionalKnapsack(List<Item> items, int capacity) {
if (capacity < 0) throw new IllegalArgumentException("negative capacity");
for (Item item : items) {
if (item.weight <= 0 || item.value < 0) throw new IllegalArgumentException("bad item");
}
List<Item> ordered = new ArrayList<>(items);
ordered.sort((first, second) -> {
long left = (long) first.value * second.weight;
long right = (long) second.value * first.weight;
int byDensity = Long.compare(right, left); // descending value / weight
if (byDensity != 0) return byDensity;
int byWeight = Integer.compare(first.weight, second.weight);
return byWeight != 0 ? byWeight : Integer.compare(first.value, second.value);
});
double totalValue = 0;
int remaining = capacity;
for (Item item : ordered) {
if (remaining == 0) break;
if (remaining >= item.weight) {
// Take whole item
totalValue += item.value;
remaining -= item.weight;
} else {
// Take fraction
totalValue += item.value * ((double) remaining / item.weight);
break;
}
}
return totalValue;
}
public static void main(String[] args) {
List<Item> items = Arrays.asList(
new Item(10, 60),
new Item(20, 100),
new Item(30, 120)
);
int capacity = 50;
double answer = fractionalKnapsack(items, capacity);
if (Math.abs(answer - 240.0) > 1e-9) throw new AssertionError();
}
}Huffman Coding (Greedy Works!)
Python
import heapq
def huffman_coding(frequencies):
"""Return deterministic (symbol, code) pairs.
Heap construction is O(k log k); walking the finished tree costs O(C),
where C is the total number of code bits materialized across symbols.
"""
if any(weight <= 0 for weight in frequencies.values()):
raise ValueError("frequencies must be positive")
if not frequencies:
return []
# node = (symbol-or-None, left, right). The remaining tuple fields give
# the heap a total deterministic order without comparing node objects.
heap = []
serial = 0
for symbol, weight in sorted(frequencies.items()):
heapq.heappush(heap, (weight, symbol, serial, (symbol, None, None)))
serial += 1
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
parent = (None, left[3], right[3])
heapq.heappush(
heap,
(left[0] + right[0], min(left[1], right[1]), serial, parent),
)
serial += 1
codes = {}
def visit(node, prefix):
symbol, left, right = node
if symbol is not None:
codes[symbol] = prefix or "0"
return
visit(left, prefix + "0")
visit(right, prefix + "1")
visit(heap[0][3], "")
return sorted(codes.items(), key=lambda pair: (len(pair[1]), pair[0]))
freq = {'a': 5, 'b': 9, 'c': 12, 'd': 13, 'e': 16, 'f': 45}
codes = huffman_coding(freq)
assert dict(codes)['f'] == '0'
for char, code in codes:
print(f"{char}: {code}")Java
import java.util.*;
public class HuffmanCoding {
static class Node implements Comparable<Node> {
int freq;
char ch;
char minChar;
Node left, right;
Node(char ch, int freq) {
this.ch = ch;
this.minChar = ch;
this.freq = freq;
}
Node(int freq, Node left, Node right) {
this.ch = 0;
this.minChar = (char) Math.min(left.minChar, right.minChar);
this.freq = freq;
this.left = left;
this.right = right;
}
@Override
public int compareTo(Node other) {
int byFrequency = Integer.compare(this.freq, other.freq);
return byFrequency != 0 ? byFrequency : Character.compare(this.minChar, other.minChar);
}
}
/**
* Greedy: Always merge two smallest frequencies
* Builds optimal prefix-free code
*
* Time: O(k log k + C), where C is total emitted code length
*/
public static Map<Character, String> huffmanCoding(Map<Character, Integer> freq) {
PriorityQueue<Node> heap = new PriorityQueue<>();
// Create leaf nodes
for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
if (entry.getValue() <= 0) throw new IllegalArgumentException("non-positive frequency");
heap.add(new Node(entry.getKey(), entry.getValue()));
}
// Build Huffman tree
while (heap.size() > 1) {
Node lo = heap.poll();
Node hi = heap.poll();
heap.add(new Node(Math.addExact(lo.freq, hi.freq), lo, hi));
}
// Generate codes
Map<Character, String> codes = new TreeMap<>();
generateCodes(heap.peek(), "", codes);
return codes;
}
private static void generateCodes(Node node, String code, Map<Character, String> codes) {
if (node == null) return;
if (node.left == null && node.right == null) {
codes.put(node.ch, code.isEmpty() ? "0" : code);
return;
}
generateCodes(node.left, code + "0", codes);
generateCodes(node.right, code + "1", codes);
}
public static void main(String[] args) {
Map<Character, Integer> freq = new HashMap<>();
freq.put('a', 5);
freq.put('b', 9);
freq.put('c', 12);
freq.put('d', 13);
freq.put('e', 16);
freq.put('f', 45);
Map<Character, String> codes = huffmanCoding(freq);
codes.forEach((ch, code) -> System.out.println(ch + ": " + code));
// Greedy merging creates optimal compression!
}
}💡 Pro Tip: Proving Greedy Correctness
To prove a greedy algorithm is correct, use one of these techniques:
- Greedy stays ahead: Show greedy solution is always ≥ any other solution at each step
- Exchange argument: Show you can transform any optimal solution into greedy solution without making it worse
- Structural induction: Prove greedy choice leads to optimal subproblem
If you can't prove it, greedy might be wrong! Use DP or other approaches instead.
✅ Pros & ❌ Cons
✅ Advantages
- ✓Simple and intuitive to understand
- ✓Fast - usually O(n log n) or O(n)
- ✓Space efficient - no memoization needed
- ✓Easy to implement once strategy is clear
- ✓Optimal for many real-world problems
❌ Disadvantages
- ✗Doesn't work for all problems
- ✗Hard to prove correctness
- ✗Can give wrong answer if not applicable
- ✗No backtracking - committed to choices
- ✗Finding greedy strategy can be difficult
💼 Real World Applications
📦 Data Compression
Huffman coding (greedy) is used in ZIP, JPEG, and MP3 compression. By greedily merging least frequent symbols, it creates optimal prefix-free codes.
Example: File compression, image encoding
🗺️ GPS Routing
Dijkstra's algorithm (greedy) finds shortest paths in GPS navigation. It finalizes the closest unsettled node and is correct only when edge weights are non-negative.
Example: Google Maps, delivery route optimization
💰 Cashier's Algorithm
For the canonical U.S. denominations (1¢, 5¢, 10¢, 25¢), repeatedly taking the largest usable coin minimizes the number of coins. Many other denomination systems are non-canonical and require DP or another exact method.
Example: Cash registers, vending machines
📅 Task Scheduling
CPU scheduling, meeting room allocation, and job scheduling use greedy strategies like "shortest job first" or "earliest deadline first" for optimization.
Example: Operating systems, calendar apps
Exercises
- Sketch a proof for earliest-finish activity selection on a three-interval instance where longest-first fails.
- Coins {1,3,4}, amount 6: show greedy vs optimum. What property of canonical coin systems restores greedy correctness?
- Why does value-density greed fail for 0/1 knapsack while an works for fractional knapsack?
Primary sources
- Cormen et al., CLRS: greedy algorithms (activity selection, Huffman; exchange arguments).
- Kleinberg & Tardos: stay-ahead and exchange proofs.
- Oxley, J.: Matroid Theory (when sorted greedy is universally optimal).
Takeaway
Greedy is not “pick the best locally.” It is a local rule plus an exchange or stay-ahead certificate. No certificate, no claim.