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.
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:
| 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) |
Greedy algorithms require proof that the greedy choice leads to an optimal solution. Two main proof techniques are exchange argument and greedy stays ahead.
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.
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.
Select maximum number of non-overlapping activities. Classic greedy problem solved by selecting earliest-ending activity repeatedly.
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
Maximize value in knapsack of capacity W. Unlike 0/1 knapsack, we can take fractional items. Greedy by value-to-weight ratio is optimal.
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}"); } }
Build optimal prefix-free binary code minimizing average code length. Greedy merges two least-frequent nodes repeatedly, creating minimum-cost tree.
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}"); } }
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.
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}"); } }
Find minimum platforms needed to accommodate all train arrivals and departures. Greedy: count overlapping intervals at peak moment.
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}"); } }
Make change with minimum coins. Greedy works for canonical coin systems (most real currency). DP required for arbitrary denominations.
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; }
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]; }
coins = {1, 3, 4}, amount = 6
Greedy: 4 + 1 + 1 = 3 coins
Optimal (DP): 3 + 3 = 2 coins
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.
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
Description: Minimum jumps to reach last index. Greedy: jump as far as possible while tracking farthest within current range.
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
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.
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; }
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.
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
Description: Unlimited transactions. Buy low, sell high. Greedy: add every positive price difference.
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
Description: Remove minimum intervals to make remaining non-overlapping. Greedy: sort by end time, remove later-ending interval when overlap.
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; }
Description: Balloons at positions [x_start, x_end]. One arrow bursts all overlapping balloons. Minimum arrows? Greedy: sort by end, shoot at rightmost point.
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; }
Description: Execute n tasks with cooldown between same tasks. Minimum time? Greedy: execute most frequent first to spread them out.
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); }
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.
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(); }
Description: Partition string so each letter in single partition. Maximum partitions? Greedy: extend partition until last occurrence of all chars seen.
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]
Description: Rearrange string so no adjacent characters are same. Greedy: use max heap, always place most frequent char, alternate.
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() : ""; }
Description: Minimum meeting rooms. Greedy: similar to minimum platforms - track overlapping intervals.
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; }
| 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 |