Chapter 13

Greedy Algorithms

Master the greedy paradigm: making locally optimal choices that lead to globally optimal solutions. Learn when greedy works, how to prove correctness with exchange arguments, and solve classic problems like Activity Selection, Huffman Coding, and Job Sequencing with complete C# implementations.

11
Core Algorithms
14+
Practice Problems
60+
Code Examples
5
Proof Techniques
Table of Contents
13.1

Greedy Paradigm Fundamentals

A greedy algorithm makes locally optimal choices at each step with the hope of finding a global optimum. Unlike dynamic programming, greedy never reconsiders past decisions.

The greedy paradigm works by:

1
Greedy Choice Property: A globally optimal solution contains a greedy choice made at the first step.
2
Optimal Substructure: An optimal solution to a problem contains optimal solutions to its subproblems.
3
No Backtracking: Once a choice is made, it's never reconsidered, making greedy more efficient than DP.

Characteristics of Greedy Problems

Property Description Example
Greedy Choice Current choice doesn't depend on future choices Pick earliest-ending activity
Optimal Substructure Solution = greedy choice + solution to subproblem Remaining activities after picking one
No Dependencies Decisions are independent of previous mistakes Activity selection is order-independent
Efficiency Usually O(n log n) or better After sorting, selection is O(n)
⚠️
Not all problems are solved greedily! The greedy approach may fail if the problem lacks the optimal substructure property. Always verify with test cases and formal proof.
13.2

Proving Greedy Correctness

Greedy algorithms require proof that the greedy choice leads to an optimal solution. Two main proof techniques are exchange argument and greedy stays ahead.

Exchange Argument

Assume an optimal solution exists that differs from the greedy solution. Show how to exchange elements to transform the optimal solution into the greedy solution without making it worse.

Exchange Argument Steps
1. Assume optimal solution OPT differs from greedy solution G

2. Find first position where they differ

3. Swap the differing element in OPT with greedy choice

4. Show cost doesn't increase (OPT' ≥ OPT)

5. Repeat until OPT becomes G
Greedy Stays Ahead
1. Define "ahead" metric (e.g., earliest end time)

2. Prove greedy is always ahead after each step

3. Show "ahead" implies optimality

4. Conclude greedy produces optimal solution

Example: Activity Selection Proof

Theorem: Greedy selection (earliest-ending activity first) produces optimal solution.

Proof (Exchange Argument): Suppose OPT is an optimal solution differing from greedy solution G. Let a₁ be the greedy choice (earliest ending). If OPT's first activity b₁ ≠ a₁, then since a₁ ends before b₁, we can replace b₁ with a₁ in OPT. The remaining activities are still compatible. Repeat this process until OPT = G, proving greedy is optimal.

13.3

Activity Selection & Interval Scheduling

Select maximum number of non-overlapping activities. Classic greedy problem solved by selecting earliest-ending activity repeatedly.

Activity Selection Algorithm
Sort activities by end time, then greedily select activities that don't overlap with previously selected ones.
13.1
Time Complexity
O(n log n)
Space Complexity
O(n)
Best Case
O(n log n)
C#
public class ActivitySelection
{
    public class Activity
    {
        public string Name { get; set; }
        public int Start { get; set; }
        public int End { get; set; }
    }

    public static List<Activity> SelectActivities(List<Activity> activities)
    {
        // Sort by end time
        var sorted = activities
            .OrderBy(a => a.End)
            .ToList();

        var selected = new List<Activity>();

        foreach (var activity in sorted)
        {
            // Check if no overlap with last selected
            if (selected.Count == 0 ||
                selected[selected.Count - 1].End <= activity.Start)
            {
                selected.Add(activity);
            }
        }

        return selected;
    }

    public static void Main()
    {
        var activities = new List<Activity>
        {
            new Activity { Name = "A", Start = 1, End = 3 },
            new Activity { Name = "B", Start = 2, End = 5 },
            new Activity { Name = "C", Start = 4, End = 6 },
            new Activity { Name = "D", Start = 6, End = 9 },
        };

        var result = SelectActivities(activities);

        Console.WriteLine("Selected Activities:");
        foreach (var a in result)
            Console.WriteLine($"{a.Name}: {a.Start}-{a.End}");
    }
}

Output:

Selected Activities:
A: 1-3
C: 4-6
D: 6-9
Key Insight: Selecting earliest-ending activity leaves maximum room for future activities. This greedy choice is always part of an optimal solution.
13.4

Fractional Knapsack Problem

Maximize value in knapsack of capacity W. Unlike 0/1 knapsack, we can take fractional items. Greedy by value-to-weight ratio is optimal.

Fractional Knapsack
Sort items by value-to-weight ratio, greedily take items starting with highest ratio.
13.2
Time Complexity
O(n log n)
Space Complexity
O(n)
C#
public class FractionalKnapsack
{
    public class Item
    {
        public string Name { get; set; }
        public double Value { get; set; }
        public double Weight { get; set; }
    }

    public static double MaxValue(List<Item> items, double capacity)
    {
        // Sort by value-to-weight ratio (descending)
        var sorted = items
            .OrderByDescending(x => x.Value / x.Weight)
            .ToList();

        double totalValue = 0;
        double remainingCapacity = capacity;

        foreach (var item in sorted)
        {
            if (remainingCapacity <= 0) break;

            if (item.Weight <= remainingCapacity)
            {
                // Take whole item
                totalValue += item.Value;
                remainingCapacity -= item.Weight;
            }
            else
            {
                // Take fraction of item
                double fraction = remainingCapacity / item.Weight;
                totalValue += fraction * item.Value;
                remainingCapacity = 0;
            }
        }

        return totalValue;
    }

    public static void Main()
    {
        var items = new List<Item>
        {
            new Item { Name = "Gold", Value = 100, Weight = 10 },
            new Item { Name = "Silver", Value = 60, Weight = 20 },
            new Item { Name = "Bronze", Value = 40, Weight = 30 },
        };

        double maxVal = MaxValue(items, 50);
        Console.WriteLine($"Maximum value: {maxVal}");
    }
}
!
Greedy fails for 0/1 Knapsack! If you cannot take fractional items, greedy by value-to-weight ratio fails. Use dynamic programming instead.
13.5

Huffman Coding

Build optimal prefix-free binary code minimizing average code length. Greedy merges two least-frequent nodes repeatedly, creating minimum-cost tree.

Huffman Coding Algorithm
Use priority queue to repeatedly merge two minimum-frequency nodes until single tree remains.
13.3
Time Complexity
O(n log n)
Space Complexity
O(n)
C#
public class HuffmanCoding
{
    public class Node : IComparable<Node>
    {
        public char? Character { get; set; }
        public int Frequency { get; set; }
        public Node Left { get; set; }
        public Node Right { get; set; }

        public int CompareTo(Node other)
        {
            return this.Frequency.CompareTo(other.Frequency);
        }
    }

    public static Dictionary<char, string> BuildHuffmanCodes(string text)
    {
        // Count frequencies
        var freq = new Dictionary<char, int>();
        foreach (char c in text)
        {
            if (freq.ContainsKey(c)) freq[c]++;
            else freq[c] = 1;
        }

        // Build priority queue with leaf nodes
        var pq = new PriorityQueue<Node, int>();
        foreach (var kvp in freq)
        {
            var node = new Node
            {
                Character = kvp.Key,
                Frequency = kvp.Value
            };
            pq.Enqueue(node, node.Frequency);
        }

        // Build tree by merging
        while (pq.Count > 1)
        {
            var left = pq.Dequeue();
            var right = pq.Dequeue();

            var parent = new Node
            {
                Frequency = left.Frequency + right.Frequency,
                Left = left,
                Right = right
            };

            pq.Enqueue(parent, parent.Frequency);
        }

        var root = pq.Dequeue();

        // Generate codes
        var codes = new Dictionary<char, string>();
        GenerateCodes(root, "", codes);

        return codes;
    }

    private static void GenerateCodes(Node node, string code,
        Dictionary<char, string> codes)
    {
        if (node == null) return;

        if (node.Character != null)
            codes[node.Character.Value] = code;

        GenerateCodes(node.Left, code + "0", codes);
        GenerateCodes(node.Right, code + "1", codes);
    }

    public static void Main()
    {
        string text = "hello world";
        var codes = BuildHuffmanCodes(text);

        Console.WriteLine("Huffman Codes:");
        foreach (var kvp in codes.OrderBy(x => x.Value.Length))
            Console.WriteLine($"{kvp.Key}: {kvp.Value}");
    }
}
Why it works: Merging lowest-frequency nodes minimizes total cost. Characters appearing more frequently get shorter codes, minimizing expected code length.
13.6

Job Sequencing with Deadlines

Schedule jobs with deadlines and profits to maximize total profit. Jobs must complete by deadline. Greedy: sort by profit (descending) and place in latest available slot.

Job Sequencing Algorithm
Sort jobs by profit, greedily place each in latest available time slot before deadline.
13.4
Time Complexity
O(n log n + n*d)
Space Complexity
O(d)
C#
public class JobSequencing
{
    public class Job
    {
        public string Id { get; set; }
        public int Deadline { get; set; }
        public int Profit { get; set; }
    }

    public static int MaxProfit(List<Job> jobs)
    {
        // Sort by profit (descending)
        var sorted = jobs
            .OrderByDescending(j => j.Profit)
            .ToList();

        int maxDeadline = sorted.Max(j => j.Deadline);
        var slots = new bool[maxDeadline + 1];
        var result = new string[maxDeadline + 1];

        int totalProfit = 0;

        foreach (var job in sorted)
        {
            // Place job in latest available slot before deadline
            for (int i = job.Deadline; i >= 1; i--)
            {
                if (!slots[i])
                {
                    slots[i] = true;
                    result[i] = job.Id;
                    totalProfit += job.Profit;
                    break;
                }
            }
        }

        return totalProfit;
    }

    public static void Main()
    {
        var jobs = new List<Job>
        {
            new Job { Id = "A", Deadline = 4, Profit = 20 },
            new Job { Id = "B", Deadline = 1, Profit = 10 },
            new Job { Id = "C", Deadline = 3, Profit = 40 },
            new Job { Id = "D", Deadline = 2, Profit = 30 },
        };

        int profit = MaxProfit(jobs);
        Console.WriteLine($"Maximum profit: {profit}");
    }
}
13.7

Minimum Platforms Required

Find minimum platforms needed to accommodate all train arrivals and departures. Greedy: count overlapping intervals at peak moment.

Minimum Platforms Algorithm
Sort arrivals and departures separately. Use two pointers to count platforms needed at each time point.
13.5
Time Complexity
O(n log n)
Space Complexity
O(1)
C#
public class MinimumPlatforms
{
    public static int FindPlatforms(int[] arrivals, int[] departures)
    {
        // Sort both arrays
        Array.Sort(arrivals);
        Array.Sort(departures);

        int platforms = 0, maxPlatforms = 0;
        int i = 0, j = 0;

        while (i < arrivals.Length && j < departures.Length)
        {
            if (arrivals[i] <= departures[j])
            {
                // Train arrives, need platform
                platforms++;
                maxPlatforms = Math.Max(maxPlatforms, platforms);
                i++;
            }
            else
            {
                // Train departs, free platform
                platforms--;
                j++;
            }
        }

        return maxPlatforms;
    }

    public static void Main()
    {
        int[] arrivals = { 900, 940, 950, 1100, 1500, 1800 };
        int[] departures = { 910, 1200, 1120, 1130, 1900, 2000 };

        int platforms = FindPlatforms(arrivals, departures);
        Console.WriteLine($"Minimum platforms needed: {platforms}");
    }
}
Key Insight: Process events (arrivals/departures) in chronological order. At any time, platforms needed = trains at station = arrivals - departures so far.
13.8

Coin Change: Greedy vs Dynamic Programming

Make change with minimum coins. Greedy works for canonical coin systems (most real currency). DP required for arbitrary denominations.

Greedy Approach (US Coins)

C#
public static int CoinChangeGreedy(int amount)
{
    int[] coins = { 25, 10, 5, 1 }; // Canonical: works greedy
    int count = 0;

    foreach (int coin in coins)
    {
        count += amount / coin;
        amount %= coin;
    }

    return count;
}

DP Approach (Arbitrary Denominations)

C#
public static int CoinChangeDP(int amount, int[] coins)
{
    var dp = new int[amount + 1];
    for (int i = 1; i <= amount; i++)
    {
        dp[i] = int.MaxValue;
        foreach (int coin in coins)
        {
            if (coin <= i && dp[i - coin] != int.MaxValue)
                dp[i] = Math.Min(dp[i], 1 + dp[i - coin]);
        }
    }
    return dp[amount];
}

Counterexample: Greedy Fails

coins = {1, 3, 4}, amount = 6

Greedy: 4 + 1 + 1 = 3 coins

Optimal (DP): 3 + 3 = 2 coins

!
Greedy Assumption: Only use greedy for canonical coin systems where larger coins are multiples of smaller ones. For arbitrary denominations, always use DP.
13.9

Practice Problems (14+ with Full C# Solutions)

Problem 1: Jump Game I

Description: Array of positive integers where each element is max jump length from position. Can reach last index? Greedy: always check if we can reach further.

C#
public static bool CanJump(int[] nums)
{
    int maxReach = 0;
    for (int i = 0; i < nums.Length; i++)
    {
        if (i > maxReach) return false;
        maxReach = Math.Max(maxReach, i + nums[i]);
    }
    return true;
}
// Example: [2,3,1,1,4] → true, [3,2,1,0,4] → false

Problem 2: Jump Game II

Description: Minimum jumps to reach last index. Greedy: jump as far as possible while tracking farthest within current range.

C#
public static int JumpII(int[] nums)
{
    int jumps = 0, currentEnd = 0, farthest = 0;
    for (int i = 0; i < nums.Length - 1; i++)
    {
        farthest = Math.Max(farthest, i + nums[i]);
        if (i == currentEnd)
        {
            jumps++;
            currentEnd = farthest;
        }
    }
    return jumps;
}
// Example: [2,3,1,1,4] → 2 jumps

Problem 3: Gas Station

Description: Start gas station where gas[i] is gas gained, cost[i] is gas cost to reach next. Complete circuit? Greedy: if total gas ≥ total cost, start from first position where tank doesn't go negative.

C#
public static int CanCompleteCircuit(int[] gas, int[] cost)
{
    int totalGas = 0, currentGas = 0, start = 0;
    foreach (int g in gas) totalGas += g;
    foreach (int c in cost) totalGas -= c;

    if (totalGas < 0) return -1;

    for (int i = 0; i < gas.Length; i++)
    {
        currentGas += gas[i] - cost[i];
        if (currentGas < 0)
        {
            start = i + 1;
            currentGas = 0;
        }
    }
    return start;
}

Problem 4: Assign Cookies

Description: Maximize happy children. Each child has min cookie size requirement g[i], cookies have sizes s[j]. Greedy: sort both, assign smallest cookie to greediest child.

C#
public static int FindContentChildren(int[] g, int[] s)
{
    Array.Sort(g);
    Array.Sort(s);

    int child = 0;
    for (int cookie = 0; cookie < s.Length && child < g.Length; cookie++)
    {
        if (s[cookie] >= g[child]) child++;
    }
    return child;
}
// Example: g=[1,2,3], s=[1,1] → 1 happy child

Problem 5: Best Time to Buy and Sell Stock II

Description: Unlimited transactions. Buy low, sell high. Greedy: add every positive price difference.

C#
public static int MaxProfit(int[] prices)
{
    int profit = 0;
    for (int i = 1; i < prices.Length; i++)
    {
        if (prices[i] > prices[i - 1])
            profit += prices[i] - prices[i - 1];
    }
    return profit;
}
// Example: [7,1,5,3,6,4] → 7

Problem 6: Non-overlapping Intervals

Description: Remove minimum intervals to make remaining non-overlapping. Greedy: sort by end time, remove later-ending interval when overlap.

C#
public static int EraseOverlapIntervals(int[][] intervals)
{
    Array.Sort(intervals, (a, b) => a[1].CompareTo(b[1]));

    int end = intervals[0][1], removed = 0;
    for (int i = 1; i < intervals.Length; i++)
    {
        if (intervals[i][0] < end)
            removed++;
        else
            end = intervals[i][1];
    }
    return removed;
}

Problem 7: Minimum Number of Arrows to Burst Balloons

Description: Balloons at positions [x_start, x_end]. One arrow bursts all overlapping balloons. Minimum arrows? Greedy: sort by end, shoot at rightmost point.

C#
public static int FindMinArrowShots(int[][] balloons)
{
    Array.Sort(balloons, (a, b) =>
    {
        if (a[1] < b[1]) return -1;
        return a[1] > b[1] ? 1 : 0;
    });

    int arrows = 1;
    long lastPos = balloons[0][1];
    for (int i = 1; i < balloons.Length; i++)
    {
        if (balloons[i][0] > lastPos)
        {
            arrows++;
            lastPos = balloons[i][1];
        }
    }
    return arrows;
}

Problem 8: Task Scheduler

Description: Execute n tasks with cooldown between same tasks. Minimum time? Greedy: execute most frequent first to spread them out.

C#
public static int LeastInterval(char[] tasks, int n)
{
    var freq = new Dictionary<char, int>();
    foreach (char task in tasks)
        freq[task] = freq.ContainsKey(task) ? freq[task] + 1 : 1;

    int maxFreq = freq.Values.Max();
    int maxCount = freq.Values.Count(v => v == maxFreq);

    return Math.Max(tasks.Length, (maxFreq - 1) * (n + 1) + maxCount);
}

Problem 9: Queue Reconstruction by Height

Description: Each person [h, k] where k is count of people taller. Reconstruct queue. Greedy: sort by height descending, then by k ascending. Insert by position.

C#
public static int[][] ReconstructQueue(int[][] people)
{
    var sorted = people
        .OrderByDescending(p => p[0])
        .ThenBy(p => p[1])
        .ToList();

    var result = new List<int[]>();
    foreach (var person in sorted)
        result.Insert(person[1], person);

    return result.ToArray();
}

Problem 10: Partition Labels

Description: Partition string so each letter in single partition. Maximum partitions? Greedy: extend partition until last occurrence of all chars seen.

C#
public static List<int> PartitionLabels(string s)
{
    var lastPos = new Dictionary<char, int>();
    for (int i = 0; i < s.Length; i++)
        lastPos[s[i]] = i;

    var result = new List<int>();
    int start = 0, end = 0;

    for (int i = 0; i < s.Length; i++)
    {
        end = Math.Max(end, lastPos[s[i]]);
        if (i == end)
        {
            result.Add(end - start + 1);
            start = i + 1;
        }
    }
    return result;
}
// Example: "ababcbacaddefegdehijhij" → [9,7,8]

Problem 11: Reorganize String

Description: Rearrange string so no adjacent characters are same. Greedy: use max heap, always place most frequent char, alternate.

C#
public static string ReorganizeString(string s)
{
    var freq = new Dictionary<char, int>();
    foreach (char c in s)
        freq[c] = freq.ContainsKey(c) ? freq[c] + 1 : 1;

    var pq = new PriorityQueue<(char, int), int>();
    foreach (var kvp in freq)
        pq.Enqueue((kvp.Key, kvp.Value), -kvp.Value);

    var result = new StringBuilder();
    (char, int) prev = (' ', 0);

    while (pq.Count > 0)
    {
        var (ch, cnt) = pq.Dequeue();
        if (prev.Item2 > 0)
            pq.Enqueue(prev, -prev.Item2);

        result.Append(ch);
        prev = (ch, cnt - 1);
    }

    return result.Length == s.Length ? result.ToString() : "";
}

Problem 12: Meeting Rooms II

Description: Minimum meeting rooms. Greedy: similar to minimum platforms - track overlapping intervals.

C#
public static int MinMeetingRooms(int[][] intervals)
{
    var starts = intervals.Select(x => x[0]).OrderBy(x => x).ToArray();
    var ends = intervals.Select(x => x[1]).OrderBy(x => x).ToArray();

    int rooms = 0, maxRooms = 0;
    int i = 0, j = 0;

    while (i < starts.Length)
    {
        if (starts[i] < ends[j])
        {
            rooms++;
            i++;
        }
        else
        {
            rooms--;
            j++;
        }
        maxRooms = Math.Max(maxRooms, rooms);
    }
    return maxRooms;
}
13.10

Greedy vs Dynamic Programming: Decision Guide

Aspect Greedy Dynamic Programming
Decision Making Make locally optimal choice once Reconsider choices via overlapping subproblems
Backtracking Never reconsidered Can change based on subproblem results
Proof Required Exchange argument or greedy-stays-ahead Optimal substructure + memoization
Time Complexity Usually O(n log n) or O(n) Usually O(n²) or O(n³)
Space Complexity O(1) or O(n) O(n) or O(n²) for memoization
When to Use Activity selection, huffman, job sequencing 0/1 knapsack, coin change, LCS

Decision Tree

Try Greedy If:
• Problem has optimal substructure
• Greedy choice property holds
• Sorting/priority helps
• No future choices affect current
• Problem is about selection/scheduling
Use DP If:
• Overlapping subproblems exist
• Greedy counterexample found
• Need optimal of all possibilities
• Order of choices matters
• Problem has "count/min/max" goal

Case Study: Coin Change

✓ Canonical Systems (US coins)
Greedy works: [25,10,5,1]
O(n log n) sorting + O(n) greedy
Proof: larger coins cover multiple smaller
✗ Arbitrary Systems
Greedy fails: [1,3,4], amount=6
Optimal: 3+3=2, Greedy: 4+1+1=3
Use DP: O(n²) or O(n·m)
13.11

Interview Tips & Patterns

Pattern 1
Sorting First
Most greedy problems require sorting. Identify the sort key: end time (activity), profit (job), height (queue), frequency (huffman).
Pattern 2
Verify with Examples
Always test greedy against small examples. Try to find a counterexample. If it fails on any case, greedy is wrong.
Pattern 3
State Greedy Choice
Clearly articulate why this greedy choice is optimal. "We pick earliest-ending activity because it leaves maximum room for future selections."
Pattern 4
Consider Edge Cases
Empty input, single element, all same, conflicts, ties. Handle gracefully.
Pattern 5
Greedy Stays Ahead
Use this proof technique: show greedy solution is always ahead by some metric. Proves optimality.
Pattern 6
Exchange Argument
Assume optimal differs from greedy. Swap elements to transform optimal into greedy. If cost doesn't increase, greedy is optimal.

Common Interview Questions

Q
Q: How do you know when to use greedy vs DP? A: Try greedy first—it's faster if it works. If you find a counterexample or can't prove optimality, switch to DP. Greedy requires proof; DP is more brute-force but guaranteed if optimal substructure exists.
Q
Q: Can you prove your greedy solution is optimal? A: Yes—use exchange argument (show any optimal solution can be transformed into greedy without increasing cost) or greedy-stays-ahead (track a metric where greedy is always ahead).
Q
Q: What if my greedy fails on a test case? A: Immediately pivot to DP or brute force. The interviewer is testing if you can adapt. Explain why greedy failed and why DP is necessary.

Implementation Checklist

1
Understand problem: What are we optimizing? What constraints exist?
2
Identify greedy choice: What's the locally optimal choice at each step?
3
Determine sort order: By what property should we sort? (time, profit, frequency, etc.)
4
Test with examples: Run on 2-3 small examples, try to break it.
5
Write proof sketch: Why does this greedy choice lead to optimal solution?
6
Code implementation: Sort, iterate, apply greedy choice, track state.
7
Analyze complexity: Time (usually O(n log n) due to sort) and space.

Red Flags