Greedy Algorithm Patterns

1. Activity Selection Problem (Interval Scheduling)

Aspect Description Complexity
Pattern Select maximum number of non-overlapping activities by always choosing the activity that finishes earliest O(n log n)
Key Insight Sorting by end time and greedily selecting earliest-finishing compatible activity gives optimal solution Proof by exchange
Approach Sort intervals by end time, iterate and select if start ≥ last selected end Single pass after sort
Use Cases Meeting room scheduling, task scheduling, interval partitioning Optimal subset

Example: Activity Selection

function activitySelection(activities) {
  // Sort by end time
  activities.sort((a, b) => a[1] - b[1]);
  
  const selected = [activities[0]];
  let lastEnd = activities[0][1];
  
  for (let i = 1; i < activities.length; i++) {
    const [start, end] = activities[i];
    // Select if non-overlapping
    if (start >= lastEnd) {
      selected.push(activities[i]);
      lastEnd = end;
    }
  }
  
  return selected;
}

// Example: [[1,3], [2,5], [4,6], [6,8], [5,7], [8,9]]
// After sorting by end: [[1,3], [2,5], [4,6], [5,7], [6,8], [8,9]]
// Selected: [[1,3], [4,6], [6,8], [8,9]]
const activities = [[1,3], [2,5], [4,6], [6,8], [5,7], [8,9]];
console.log(activitySelection(activities)); // 4 activities
Note: Why sort by end time? If we sort by start time or duration, we might select a long activity that blocks many shorter ones. Earliest finish time ensures maximum room for future selections.

2. Jump Game Reachability Problem

Variant Problem Greedy Strategy
Jump Game I Can reach last index from index 0? Track maximum reachable position, update at each step
Jump Game II Minimum jumps to reach end BFS-like approach: jump when current range ends, count levels
Key Insight At each position, maximize reach; if current position unreachable, return false O(n) time, O(1) space

Example: Jump Game I - Can Reach End

function canJump(nums) {
  let maxReach = 0;
  
  for (let i = 0; i < nums.length; i++) {
    // If current position is beyond max reach, can't proceed
    if (i > maxReach) return false;
    
    // Update max reachable position
    maxReach = Math.max(maxReach, i + nums[i]);
    
    // Early exit if can reach end
    if (maxReach >= nums.length - 1) return true;
  }
  
  return true;
}

console.log(canJump([2,3,1,1,4])); // true
console.log(canJump([3,2,1,0,4])); // false

Example: Jump Game II - Minimum Jumps

function minJumps(nums) {
  if (nums.length <= 1) return 0;
  
  let jumps = 0;
  let currentEnd = 0;    // End of current jump range
  let farthest = 0;      // Farthest we can reach
  
  for (let i = 0; i < nums.length - 1; i++) {
    // Update farthest reachable position
    farthest = Math.max(farthest, i + nums[i]);
    
    // When we reach end of current range, must jump
    if (i === currentEnd) {
      jumps++;
      currentEnd = farthest;
      
      // Early exit if can reach end
      if (currentEnd >= nums.length - 1) break;
    }
  }
  
  return jumps;
}

// Example: [2,3,1,1,4]
// i=0: farthest=2, jump to range [1,2], jumps=1
// i=1: farthest=4, i=2: reach end of range, jump to range [3,4], jumps=2
console.log(minJumps([2,3,1,1,4])); // 2

3. Gas Station Problem (Circular Array)

Concept Description Complexity
Problem Find starting gas station to complete circular route, given gas[] and cost[] arrays O(n) time
Greedy Insight If total gas ≥ total cost, solution exists. Start from position where running tank never goes negative One pass solution
Key Observation If we can't reach station j from i, then any station between i and j also can't reach j Skip entire segment

Example: Gas Station Circuit

function canCompleteCircuit(gas, cost) {
  let totalGas = 0, totalCost = 0;
  let tank = 0, start = 0;
  
  for (let i = 0; i < gas.length; i++) {
    totalGas += gas[i];
    totalCost += cost[i];
    tank += gas[i] - cost[i];
    
    // If tank goes negative, can't start from 'start'
    // Try next position as new starting point
    if (tank < 0) {
      start = i + 1;
      tank = 0;
    }
  }
  
  // If total gas < total cost, no solution
  return totalGas >= totalCost ? start : -1;
}

const gas = [1,2,3,4,5];
const cost = [3,4,5,1,2];
console.log(canCompleteCircuit(gas, cost)); // 3
// Starting at index 3: tank=4-1=3, then 3+5-2=6, then 6+1-3=4...
Note: The key greedy choice is: if we fail at position j starting from i, we skip all positions between i and j as potential starts, because they have even less fuel accumulated than position i.

4. Interval Scheduling Maximization

Problem Type Strategy Sorting Key
Max Non-overlapping Select maximum count of intervals Sort by end time (earliest finish)
Min Removal Remove minimum intervals to make non-overlapping Sort by end time, count overlaps to remove
Min Meeting Rooms Minimum rooms needed for all meetings Sort starts and ends separately, sweep line
Merge Intervals Merge all overlapping intervals Sort by start time, merge consecutive overlaps

Example: Minimum Interval Removals

function eraseOverlapIntervals(intervals) {
  if (intervals.length === 0) return 0;
  
  // Sort by end time
  intervals.sort((a, b) => a[1] - b[1]);
  
  let removals = 0;
  let lastEnd = intervals[0][1];
  
  for (let i = 1; i < intervals.length; i++) {
    const [start, end] = intervals[i];
    
    if (start < lastEnd) {
      // Overlapping - remove current interval
      removals++;
      // Keep the one with earlier end (already sorted)
    } else {
      // Non-overlapping - update end
      lastEnd = end;
    }
  }
  
  return removals;
}

console.log(eraseOverlapIntervals([[1,2],[2,3],[3,4],[1,3]])); // 1
// Remove [1,3] to make all non-overlapping

Example: Minimum Meeting Rooms

function minMeetingRooms(intervals) {
  if (intervals.length === 0) return 0;
  
  const starts = intervals.map(i => i[0]).sort((a, b) => a - b);
  const ends = intervals.map(i => i[1]).sort((a, b) => a - b);
  
  let rooms = 0, endPtr = 0;
  
  for (let i = 0; i < starts.length; i++) {
    // If meeting starts before earliest ending, need new room
    if (starts[i] < ends[endPtr]) {
      rooms++;
    } else {
      // A room becomes available
      endPtr++;
    }
  }
  
  return rooms;
}

console.log(minMeetingRooms([[0,30],[5,10],[15,20]])); // 2

5. Job Sequencing with Deadlines

Concept Description Approach
Problem Given jobs with deadlines and profits, maximize profit by scheduling one job per unit time Greedy selection
Strategy Sort jobs by profit (descending), assign each to latest available slot before deadline O(n²) or O(n log n)
Data Structure Use array/set to track occupied time slots, or disjoint set for optimization Union-Find for O(n log n)

Example: Job Sequencing Problem

function jobSequencing(jobs) {
  // jobs = [[id, deadline, profit], ...]
  // Sort by profit descending
  jobs.sort((a, b) => b[2] - a[2]);
  
  const maxDeadline = Math.max(...jobs.map(j => j[1]));
  const slots = new Array(maxDeadline).fill(null);
  
  let totalProfit = 0;
  const sequence = [];
  
  for (const [id, deadline, profit] of jobs) {
    // Find latest available slot before deadline
    for (let i = Math.min(deadline - 1, maxDeadline - 1); i >= 0; i--) {
      if (slots[i] === null) {
        slots[i] = id;
        totalProfit += profit;
        sequence.push(id);
        break;
      }
    }
  }
  
  return { totalProfit, sequence };
}

const jobs = [
  ['a', 2, 100],
  ['b', 1, 19],
  ['c', 2, 27],
  ['d', 1, 25],
  ['e', 3, 15]
];
console.log(jobSequencing(jobs));
// { totalProfit: 142, sequence: ['a', 'c', 'e'] }

Example: Job Sequencing with Disjoint Set (Optimized)

class DisjointSet {
  constructor(n) {
    this.parent = Array.from({length: n}, (_, i) => i);
  }
  
  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }
  
  union(x, y) {
    this.parent[this.find(x)] = this.find(y);
  }
}

function jobSequencingOptimized(jobs) {
  jobs.sort((a, b) => b[2] - a[2]);
  
  const maxDeadline = Math.max(...jobs.map(j => j[1]));
  const ds = new DisjointSet(maxDeadline + 1);
  
  let totalProfit = 0;
  
  for (const [id, deadline, profit] of jobs) {
    // Find latest available slot
    const slot = ds.find(Math.min(deadline, maxDeadline));
    
    if (slot > 0) {
      ds.union(slot, slot - 1); // Mark as occupied, point to previous
      totalProfit += profit;
    }
  }
  
  return totalProfit;
}

console.log(jobSequencingOptimized(jobs)); // 142

6. Greedy Choice Property Proof

Property Description Verification
Greedy Choice Locally optimal choice leads to globally optimal solution Proof by exchange argument
Optimal Substructure Optimal solution contains optimal solutions to subproblems Required for correctness
Matroid Structure Problem exhibits matroid properties (hereditary, exchange) Theoretical foundation
No Backtracking Once a choice is made, it's never reconsidered Key difference from DP

Example: Proving Greedy Choice - Fractional Knapsack

// Greedy works for fractional knapsack (not 0/1)
function fractionalKnapsack(items, capacity) {
  // items = [[value, weight], ...]
  // Sort by value-to-weight ratio descending
  items.sort((a, b) => (b[0]/b[1]) - (a[0]/a[1]));
  
  let totalValue = 0;
  let remainingCapacity = capacity;
  
  for (const [value, weight] of items) {
    if (remainingCapacity === 0) break;
    
    // Take as much as possible
    const taken = Math.min(remainingCapacity, weight);
    totalValue += (value / weight) * taken;
    remainingCapacity -= taken;
  }
  
  return totalValue;
}

const items = [[60, 10], [100, 20], [120, 30]];
console.log(fractionalKnapsack(items, 50)); // 240
// Take all of item 2 (ratio=6), all of item 1 (ratio=5), 
// and 2/3 of item 3 (ratio=4) = 100 + 60 + 80 = 240

When Greedy Works

  • Activity Selection: Sort by end time, select earliest finish
  • Huffman Coding: Merge two smallest frequencies repeatedly
  • Dijkstra's Algorithm: Always pick nearest unvisited node
  • Prim's/Kruskal's MST: Add minimum weight edge that doesn't create cycle
  • Fractional Knapsack: Sort by value/weight ratio
  • Coin Change (canonical): Use largest denomination first
Warning: When Greedy Fails
  • 0/1 Knapsack: Need DP, greedy gives wrong answer
  • Longest Path: NP-hard, greedy doesn't work
  • Non-canonical Coin Systems: e.g., {1,3,4}, greedy fails for 6
  • Traveling Salesman: Greedy gives approximation, not optimal

Example: Huffman Coding (Greedy Correctness)

class MinHeap {
  constructor() { this.heap = []; }
  
  push(val) {
    this.heap.push(val);
    this.bubbleUp(this.heap.length - 1);
  }
  
  pop() {
    if (this.heap.length === 1) return this.heap.pop();
    const min = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.bubbleDown(0);
    return min;
  }
  
  bubbleUp(i) {
    while (i > 0) {
      const p = Math.floor((i - 1) / 2);
      if (this.heap[p].freq <= this.heap[i].freq) break;
      [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
      i = p;
    }
  }
  
  bubbleDown(i) {
    while (true) {
      let min = i;
      const left = 2 * i + 1, right = 2 * i + 2;
      if (left < this.heap.length && this.heap[left].freq < this.heap[min].freq) min = left;
      if (right < this.heap.length && this.heap[right].freq < this.heap[min].freq) min = right;
      if (min === i) break;
      [this.heap[i], this.heap[min]] = [this.heap[min], this.heap[i]];
      i = min;
    }
  }
  
  size() { return this.heap.length; }
}

function huffmanCoding(freqMap) {
  const heap = new MinHeap();
  
  // Create leaf nodes
  for (const [char, freq] of Object.entries(freqMap)) {
    heap.push({ char, freq, left: null, right: null });
  }
  
  // Build tree by merging two smallest
  while (heap.size() > 1) {
    const left = heap.pop();
    const right = heap.pop();
    
    heap.push({
      char: null,
      freq: left.freq + right.freq,
      left,
      right
    });
  }
  
  // Generate codes
  const codes = {};
  function traverse(node, code = '') {
    if (node.char) {
      codes[node.char] = code || '0';
      return;
    }
    if (node.left) traverse(node.left, code + '0');
    if (node.right) traverse(node.right, code + '1');
  }
  
  traverse(heap.pop());
  return codes;
}

const freq = { 'a': 5, 'b': 9, 'c': 12, 'd': 13, 'e': 16, 'f': 45 };
console.log(huffmanCoding(freq));
// { f: '0', c: '100', d: '101', a: '1100', b: '1101', e: '111' }
Note: To prove greedy correctness, use exchange argument: Assume optimal solution differs from greedy choice. Show you can exchange optimal's choice with greedy's without making solution worse. Repeat until greedy solution is reached, proving it's optimal.

Summary: Greedy Algorithm Pattern

  • Core Principle: Make locally optimal choice at each step, never backtrack
  • Activity Selection: Sort by end time, select earliest finish - maximizes remaining time
  • Jump Game: Track maximum reachable position; for min jumps, use BFS-like levels
  • Gas Station: If total gas ≥ cost, solution exists; start where tank never goes negative
  • Interval Scheduling: Various strategies depending on goal (max count, min rooms, min removals)
  • Job Sequencing: Sort by profit, assign to latest available slot before deadline
  • Verification: Prove greedy choice property and optimal substructure; use exchange argument
  • Key Insight: Greedy works when local optimum leads to global optimum - not all problems have this property