Skip to content

Greedy Choice Paradigms & Interval Scheduling

What it is

A greedy algorithm commits to a locally best choice at each step without revisiting earlier choices. A greedy choice is optimal only when a proof, often an exchange argument, shows that some optimal solution contains the choice made by the algorithm.

How it works

Interval scheduling chooses the maximum number of non-overlapping intervals. The algorithm sorts intervals by increasing finish time, keeps the first interval, and then keeps each interval that starts no earlier than the last selected interval. Replacing the first compatible interval in an optimal schedule with the earliest-finishing interval cannot remove any later choice, because the replacement finishes no later. Repeating that argument proves optimality.

The same proof structure appears in other greedy paradigms. Fractional knapsack repeatedly takes the highest value-to-weight ratio because any fractional solution can exchange a smaller item for the remaining fraction of that item. Huffman coding repeatedly combines the two least frequent symbols, and Prim’s and Kruskal’s algorithms repeatedly add the cheapest edge that preserves a partial solution.

The implementations expose one select operation that returns the chosen intervals in start-time order. They require intervals whose start is less than or equal to their finish, sort by finish time with start time as the tie-breaker, and allow touching endpoints. In C, select writes the result length through selected_size, and the returned array belongs to the caller.

    flowchart TD
    A[Intervals] --> B[Sort by end time, then start time]
    B --> C{Unprocessed interval?}
    C -->|No| D[Return selected intervals]
    C -->|Yes| E[Starts at or after last selected end?]
    E -->|No| C
    E -->|Yes| F[Select interval and update last end]
    F --> C
  
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ActivitySelection {
    public record Interval(int start, int end) {}

    public static List<Interval> select(List<Interval> intervals) {
        List<Interval> candidates = new ArrayList<>(intervals);
        candidates.sort(Comparator.comparingInt(Interval::end).thenComparingInt(Interval::start));
        List<Interval> selected = new ArrayList<>();
        int lastEnd = Integer.MIN_VALUE;
        for (Interval interval : candidates) {
            if (interval.start() >= lastEnd) {
                selected.add(interval);
                lastEnd = interval.end();
            }
        }
        return selected;
    }
}
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct {
    int start;
    int end;
} Interval;

static int compare_intervals(const void *left, const void *right) {
    const Interval *a = left;
    const Interval *b = right;
    if (a->end != b->end) return a->end < b->end ? -1 : 1;
    if (a->start != b->start) return a->start < b->start ? -1 : 1;
    return 0;
}

Interval *activity_selection_select(const Interval *intervals, size_t size, size_t *selected_size) {
    Interval *candidates = malloc(size * sizeof(Interval));
    Interval *selected = malloc(size * sizeof(Interval));
    if (size > 0 && (candidates == NULL || selected == NULL)) abort();
    for (size_t i = 0; i < size; i++) candidates[i] = intervals[i];
    qsort(candidates, size, sizeof(Interval), compare_intervals);
    size_t count = 0;
    int last_end = INT_MIN;
    for (size_t i = 0; i < size; i++) {
        if (candidates[i].start >= last_end) {
            selected[count++] = candidates[i];
            last_end = candidates[i].end;
        }
    }
    free(candidates);
    *selected_size = count;
    return selected;
}
class Interval:
    def __init__(self, start, end):
        self.start = start
        self.end = end


class ActivitySelection:
    @staticmethod
    def select(intervals):
        candidates = sorted(intervals, key=lambda interval: (interval.end, interval.start))
        selected = []
        last_end = float("-inf")
        for interval in candidates:
            if interval.start >= last_end:
                selected.append(interval)
                last_end = interval.end
        return selected
#[derive(Clone)]
pub struct Interval {
    pub start: i32,
    pub end: i32,
}

pub struct ActivitySelection;

impl ActivitySelection {
    pub fn select(intervals: &[Interval]) -> Vec<Interval> {
        let mut candidates = intervals.to_vec();
        candidates.sort_by_key(|interval| (interval.end, interval.start));
        let mut selected = Vec::new();
        let mut last_end = i32::MIN;
        for interval in candidates {
            if interval.start >= last_end {
                last_end = interval.end;
                selected.push(interval);
            }
        }
        selected
    }
}
export type Interval = [number, number];

export class ActivitySelection {
  static select(intervals: Interval[]): Interval[] {
    const candidates = intervals
      .map((interval) => [...interval] as Interval)
      .sort((a, b) => a[1] - b[1] || a[0] - b[0]);
    const selected: Interval[] = [];
    let lastEnd = -Infinity;
    for (const [start, end] of candidates) {
      if (start >= lastEnd) {
        selected.push([start, end]);
        lastEnd = end;
      }
    }
    return selected;
  }
}
package greedyselection

import "sort"

type Interval struct {
    Start int
    End   int
}

type ActivitySelection struct{}

func (ActivitySelection) Select(intervals []Interval) []Interval {
    candidates := append([]Interval(nil), intervals...)
    sort.Slice(candidates, func(i, j int) bool {
        if candidates[i].End == candidates[j].End {
            return candidates[i].Start < candidates[j].Start
        }
        return candidates[i].End < candidates[j].End
    })
    selected := make([]Interval, 0, len(candidates))
    lastEnd := 0
    for index, interval := range candidates {
        if index == 0 || interval.Start >= lastEnd {
            selected = append(selected, interval)
            lastEnd = interval.End
        }
    }
    return selected
}

Complexity

AlgorithmTimeExtra space
Interval schedulingO(n log n)O(n), including the selected output
Fractional knapsack after sortingO(n log n)O(n) or O(1) depending on the sort
Huffman coding with a binary heapO(n log n)O(n)
Prim’s algorithm with a binary heapO((V + E) log V)O(V)

When to use

  • You can prove that an optimal solution can begin with the local choice the algorithm makes.
  • An exchange argument can transform an optimal solution to include each greedy choice.
  • Early commitments are never invalidated by later choices, as in interval scheduling.
  • The input can be sorted or searched for the next greedy candidate efficiently.
  • You need a fast solution and can establish the optimality property rather than treating the rule as a heuristic.

Alternatives

  • Dynamic programming — handles overlapping choices by storing results for subproblems, but usually costs more time and space.
  • Branch and bound — explores alternatives when a greedy proof does not apply and prunes them with a cost bound.
  • Approximation algorithm — guarantees a computable relationship to the optimum for large problems, but does not guarantee the exact optimum.

Related