SwadhyAI Notes

All DSA notes

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

1

Name the local rule

Earliest finish, lightest safe edge, …

2

Pick a certificate

Exchange, stay-ahead, or safe cut.

3

Remaining instance stays well-formed.

4

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

Activity
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Selected
Checking
Available/Rejected

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.

Activities: [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11), (8,12), (2,14), (12,16)]

Python

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

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 ruleSmall failureLesson
Activity: shortest duration firstA short middle interval can block one compatible interval on each sideEarliest finish, not shortest length, admits the exchange proof
Activity: earliest start firstOne very long early activity blocks many later activitiesOptimize the future frontier, not the immediate timestamp
0/1 knapsack: density firstAn indivisible high-density item can prevent a better combinationDensity is safe only when fractions are allowed
Dijkstra with a negative edgeA finalized distance can later improve through that edgeThe 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. 1. Brute-force tiny inputs: enumerate every feasible subset/order and compare its optimum with greedy.
  2. 2. Generate adversarial families: ties, nested intervals, one dominant item, equal ratios, disconnected graphs, and boundary values.
  3. 3. Check invariants after each choice: feasibility never breaks and the proof's “stays-ahead” measure holds.
  4. 4. Shrink failures: minimize a random counterexample until the missing proof premise is obvious.
  5. 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

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

Java

java
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

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

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:

  1. Greedy stays ahead: Show greedy solution is always ≥ any other solution at each step
  2. Exchange argument: Show you can transform any optimal solution into greedy solution without making it worse
  3. 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

  1. Sketch a proof for earliest-finish activity selection on a three-interval instance where longest-first fails.
  2. Coins {1,3,4}, amount 6: show greedy vs optimum. What property of canonical coin systems restores greedy correctness?
  3. 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.