Cairn
algorithms · greedy · O(n log n)

back to Greedy

Interval Partitioning

Given a set of talks, each with a start and end time, how many rooms does a conference need to run every single one of them, with no two talks ever sharing a room at overlapping times? This is interval partitioning (also called minimum meeting rooms): a third distinct question over the same kind of interval input the site's other Greedy entries already answer. Activity Selection picks the largest non-conflicting subset for one resource, discarding the rest. Interval Point Cover finds the fewest points that touch every interval. This page drops everyone instead — every talk runs, on schedule, in full — and asks only how many parallel resources that takes.

The greedy idea: sort every interval by start time, then walk the list once. For each interval, check whether any currently open room's most recent talk has already ended by this one's start time. If one has, reuse whichever such room frees soonest. If none has, open a new room. This is the site's eighth entry filed under Greedy, and unlike the other seven, its proof isn't an exchange argument at all — see Why it works below.

Try it

Type a list of start-end talks (comma-separated), pick a room-assignment rule, and press Load, then Step or Run. Rooms appear as new rows only when a talk actually needs one. A green bar means the talk reused an existing room; a soft-tan bar means it forced a brand new room open; a red dashed bar means the demo's own independent check — which re-examines every pair of talks ever placed in the same room, not just the algorithm's own bookkeeping — found a real double-booking.

rooms

Press Load.

Why it works

Five of the site's other nine Greedy entries — Huffman Coding, Activity Selection, Fractional Knapsack, Job Sequencing, and Interval Point Cover — prove optimality with an exchange argument — take an optimal solution that disagrees with greedy, swap greedy's choice in, show nothing gets worse. (The remaining three take different shapes entirely: Coin Change is only exact for the right denominations, Set Cover trades exactness for a provable ratio bound, and Greedy Coloring doesn't even get that.) Interval Partitioning doesn't need an exchange argument either, because there's a bound that pins the answer down from both directions at once. Let D be the largest number of talks active at any single instant (the maximum "depth" of overlap). Lower bound: those D talks all overlap each other at that instant, so no two of them can share a room — any valid schedule needs at least D rooms, full stop, independent of any algorithm. Upper bound: processing talks in start order, the greedy rule only ever opens a (k+1)-th room for a talk when all k existing rooms are simultaneously still busy at that talk's start time — meaning k+1 talks (the new one plus one per busy room) are genuinely active together right then, so D ≥ k+1. Greedy can therefore never open more than D rooms. Since no schedule can use fewer than D and greedy never uses more, greedy hits D exactly, on every input. Which specific free room gets reused doesn't affect this count at all — "reuse the one that frees soonest" is just the standard, heap-friendly way to always find a free room fast, not a requirement of the proof.

On the demo's own eight-talk default set, this rule opens exactly 3 rooms. Talk 1 spans the whole conference (0–30), so it alone accounts for one room throughout — but between times 6 and 10, talks 1, 2, and 7 are all in progress at once, the deepest overlap anywhere in the set, which is exactly why a third room is unavoidable (and, separately, why a fourth never is). The two rules below don't reach 3 on the same data — see Pitfalls.

Reference implementation

A real implementation keeps open rooms in a small binary min-heap, keyed by when each one next frees, so "find the room that frees soonest" and "update it" are both O(log n) instead of a linear scan:

function intervalPartitioning(talks) {
  const sorted = [...talks].sort((a, b) => a.start - b.start);

  // Binary min-heap of open rooms, keyed by room.end.
  const heap = [];
  const swap = (i, j) => { [heap[i], heap[j]] = [heap[j], heap[i]]; };
  const siftUp = (i) => {
    while (i > 0) {
      const p = (i - 1) >> 1;
      if (heap[p].end <= heap[i].end) break;
      swap(p, i); i = p;
    }
  };
  const siftDown = (i) => {
    const n = heap.length;
    for (;;) {
      let s = i, l = 2 * i + 1, r = 2 * i + 2;
      if (l < n && heap[l].end < heap[s].end) s = l;
      if (r < n && heap[r].end < heap[s].end) s = r;
      if (s === i) break;
      swap(s, i); i = s;
    }
  };

  let roomCount = 0;
  const assignment = [];
  for (const talk of sorted) {
    if (heap.length && heap[0].end <= talk.start) {
      heap[0].end = talk.end;         // reuse the soonest-freeing room
      siftDown(0);
      assignment.push({ ...talk, room: heap[0].id });
    } else {
      const room = { id: roomCount++, end: talk.end };
      heap.push(room);
      siftUp(heap.length - 1);        // open a new room
      assignment.push({ ...talk, room: room.id });
    }
  }
  return { roomsNeeded: roomCount, assignment };
}

Pitfalls

Only checking the most recently used room, instead of scanning every open room, wastes rooms — but never double-books one. Switch the demo's rule to only check the most recently used room on the unchanged default data: every talk still lands in a genuinely free room (this variant never produces an invalid schedule), but it opens 6 rooms instead of 3. The failure mode is concrete on this exact data: talk 0–5 opens room 1 and immediately finishes. Room 1 then sits free for the rest of the conference, but because every later talk only ever checks whichever room was used last — not room 1 specifically — it never gets reconsidered, and the demo keeps opening brand new rooms instead. Checked beyond this one example: across 20,000 randomized trials (2–11 talks each, times 0–19), this variant opened more rooms than the true minimum in 14,002 of them (70%), confirmed against an independently computed maximum-overlap depth, never merely close.

Comparing a candidate room's free time against the new talk's end, instead of its start, produces an actively double-booked schedule. Switch the demo's rule to reuse if the room frees before this talk ends on the unchanged default data: the very second talk processed, 0–30, gets placed in the same room as the first, 0–5, because 5 ≤ 30 reads as "free enough" under this rule — even though the new talk starts at 0, before the room's occupant has even begun to leave. The demo's own end-of-run check, which independently re-examines every pair of talks sharing a room for actual time overlap rather than trusting the assignment loop's own bookkeeping, flags both talks red. This is an easy field mix-up to make by hand — "is the room free for this talk" sounds like it should compare against something about the talk, and a candidate's own end field is sitting right there — but the room only being free at the talk's end makes it free for what comes after this talk, not for the talk itself, which needs the room free already at its start. Checked beyond the hand-traced example: across the same 20,000-trial sweep, this variant produced at least one genuine double-booking in 17,020 of them (85%).

Complexity

Time: O(n log n) — the initial sort, plus one O(log n) heap operation per talk (a possible pop-and-repush to update the soonest-freeing room's key, or a single push to open a new one). Space: O(n) for the sorted copy, the heap, and the returned room assignment. The two buggy rules above are no cheaper — the difference is entirely in which room gets picked, not how many comparisons it costs.

See Choosing a Greedy Strategy for how this entry's proof compares against the site's other nine Greedy entries — short version: this is the guide's first entry that's exact by matching a lower bound to an upper bound instead of by exchange argument, though it's no less airtight for it.