Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 27 – Greedy algorithms and proofs

Activity selection and interval partitioning

Pick the most activities for one room by earliest finish, give each activity a room with the fewest rooms, and see why weighted ones need dynamic programming.

  • Intermediate
  • 40 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 13 of 40 minutes free
Free: Two questions about one Saturday · The brute force, and why it fails at these sizes · What to notice · 2 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 27 minutes.

What you will learn

  • Select the most non-overlapping activities for one room by earliest finish
  • Schedule every activity into the fewest rooms with a min-heap of free times
  • Prove both scans optimal and break plausible rules with an exhaustive search
  • Recognise fee-weighted versions that need dynamic programming instead

Before you start

Activity selection asks for the most activities one room can hold when no two may overlap. The greedy answer is to sort the activities by finish time and keep each one that starts at or after the finish of the last one kept: one sort and one scan, O(n log n). Interval partitioning asks the opposite question. Every activity must run, so how few rooms will do, and which room gets each one? Sort by start instead, keep the rooms in a min-heap ordered by the minute each becomes free, and give each activity the room that frees earliest if it is already free, or a new room if it is not. The rooms used then equal the most activities running at one moment, which no plan can beat. Both shortcuts stop working once activities carry different values, such as fees, and dynamic programming takes over.

Two questions about one Saturday

The problem

A tuition centre collects batch requests for Saturday. A batch is a pair (start, end) of minutes after midnight and holds a classroom from start up to, but not including, end, so one batch may start in the minute another ends. Question 1: with one classroom, return the most batches it can hold. Question 2: every batch must run; give each batch a room number so that no two batches in one room overlap, with as few rooms as possible. Limits: 1 ≤ n ≤ 100,000 and 0 ≤ start < end ≤ 10⁹.

  1. The eight requests in the figure: one room holds five of them, and all eight run in three rooms.
  2. [(600, 660), (660, 720)]: back to back, so one room holds both.
  3. A single batch: one kept, in room 1.
Eight tuition batches on a time line from 08:00 to 14:30: five fit in one room, and all eight fit in three rooms.One Saturday of batch requests33roomMaths1Physics2Chemistry1English3Biology2Coding1Hindi2Economics3891011121314hourkept with one room (5 batches)left out with one roomRight: each batch's room when all eight run.Shaded, marked 3: three batches at once.

The Saturday requests, the five batches one room can hold, and a room for every batch

Text description of the diagram

The figure draws the eight batch requests of one Saturday as bars against an hour axis from 08:00 to 14:30, one row each, with the room each batch gets when every batch must run written at the right.

  1. Maths, 08:00 to 10:00: one room holds it; room 1.
  2. Physics, 09:00 to 10:30: left out with one room; room 2.
  3. Chemistry, 10:00 to 11:00: one room holds it; room 1.
  4. English, 10:15 to 12:15: left out with one room; room 3.
  5. Biology, 11:00 to 12:00: one room holds it; room 2.
  6. Coding, 11:30 to 13:30: left out with one room; room 1.
  7. Hindi, 12:00 to 13:00: one room holds it; room 2.
  8. Economics, 13:00 to 14:30: one room holds it; room 3.

Two shaded bands, each marked 3, show the moments when three batches run at once: 10:15 to 10:30 (physics, chemistry and English) and 11:30 to 12:15 (English and coding, with biology until 12:00 and Hindi from 12:00). No plan can use fewer than three rooms, and the room numbers on the right use exactly three. The five batches with solid outlines, maths, chemistry, biology, Hindi and economics, run back to back in one room, and no room can hold six.

The brute force, and why it fails at these sizes

For question 1, try the subsets from the largest down until one has no overlap. For question 2, try one room, then two, then three, each time with every way of putting each batch in a room:

Every subset and every room plan, counted Python · brute/every_plan.py
import math
from itertools import combinations, product

SATURDAY = [(480, 600), (540, 630), (600, 660), (615, 735), (660, 720), (690, 810), (720, 780), (780, 870)]


def fits(batches):
    """No two of these batches overlap (half-open, so back to back is fine)."""
    ordered = sorted(batches)
    return all(earlier[1] <= later[0] for earlier, later in zip(ordered, ordered[1:]))


def most_by_subsets(batches):
    """Tries the subsets from the largest down; returns the best size and how many subsets it checked."""
    checked = 0
    for size in range(len(batches), 0, -1):
        for chosen in combinations(batches, size):
            checked += 1
            if fits(chosen):
                return size, checked
    return 0, checked


def rooms_by_assignments(batches):
    """Tries 1 room, then 2, ...: every way to put each batch in a room; returns the rooms and the plans tried per k."""
    tried = []
    for k in range(1, len(batches) + 1):
        tried.append(0)
        for plan in product(range(k), repeat=len(batches)):
            tried[-1] += 1
            if all(fits([b for b, r in zip(batches, plan) if r == room]) for room in range(k)):
                return k, tried
    return 0, tried


size, checked = most_by_subsets(SATURDAY)
print(f"one room: {size} batches, after checking {checked} of the {2 ** len(SATURDAY)} subsets")
k, tried = rooms_by_assignments(SATURDAY)
steps = [f"{t:,} plan{'s' * (t != 1)} with {r} room{'s' * (r != 1)}" for r, t in enumerate(tried, 1)]
print(f"every batch: {k} rooms, after trying {', '.join(steps)}")
for n in (20, 40):
    print(f"{n} batches: {2 ** n:.1e} subsets, and {3 ** n:.1e} plans with 3 rooms")
print(f"100,000 batches: 2 ** 100,000 subsets, a number of {math.floor(100_000 * math.log10(2)) + 1:,} digits")

Output

one room: 5 batches, after checking 66 of the 256 subsets
every batch: 3 rooms, after trying 1 plan with 1 room, 256 plans with 2 rooms, 901 plans with 3 rooms
20 batches: 1.0e+06 subsets, and 3.5e+09 plans with 3 rooms
40 batches: 1.1e+12 subsets, and 1.2e+19 plans with 3 rooms
100,000 batches: 2 ** 100,000 subsets, a number of 30,103 digits

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 every_plan.py

Eight batches already cost 66 subsets and 1,158 room plans. Each extra batch doubles the subsets and triples the plans for three rooms: 40 batches mean about 10¹² subsets and 10¹⁹ plans, and 100,000 batches a number of subsets with 30,103 digits. A quadratic method for question 1, comparing every pair of batches once, needs about n²/2 = 5 × 10⁹ steps at n = 100,000.

What to notice

  • The batch that ends first is a safe first pick for one room. Any best plan starts with a batch that ends no earlier, so swapping it for the first finisher loses nothing.
  • A crowded moment sets a floor. From 10:15 to 10:30 physics, chemistry and English all run, so no plan can use fewer than three rooms.

The key idea: two greedy scans

Activity selection, earliest finish first. Sort the batches by end. Keep the first, then keep each batch that starts at or after the end of the last batch kept, and skip the rest. On Saturday the scan keeps maths, chemistry, biology, Hindi and economics, five batches back to back. The test is >=, because a batch may start in the minute the last one ends.

Interval partitioning, earliest start first. Sort the batches by start and keep a min-heap of (minute free, room number) pairs, one per room opened. Each batch looks only at the top of the heap. If that room is free by the batch’s start, the batch takes it; if even the room that frees earliest is still busy, every room is, and the batch opens a new one. That is one sort plus one or two heap operations per batch, O(n log n) in all. The Saturday run:

Batch, by start Room that frees first Room given
Maths 08:00 none yet 1, a new room
Physics 09:00 1, busy until 10:00 2, a new room
Chemistry 10:00 1, free at 10:00 1
English 10:15 2, busy until 10:30 3, a new room
Biology 11:00 2, free at 10:30 2
Coding 11:30 1, free at 11:00 1
Hindi 12:00 2, free at 12:00 2
Economics 13:00 3, free at 12:15 3

Three rooms is the floor that the crowded moment set, so no plan does better.

Other rules, and fees. Some rules look as good and are not. Earliest start first keeps the batch that starts first. Shortest first keeps the shortest batch. Fewest clashes first keeps the batch that overlaps the fewest others. For rooms, end order fills rooms in order of finish instead of start. When batches pay fees, the goal becomes the most money. Highest fee first keeps the dearest batch; best fee per minute keeps the batch that pays most for each minute. Both fail, and so does earliest end. A dynamic programme over the batches sorted by end, with binary search, finds the best total in O(n log n).

Exercise: a room for every batch

Exercise · Medium · Python

A room for every tuition batch, with the fewest rooms

A tuition centre must run every batch it has accepted. Each batch is a pair (start, end) of whole minutes, and a batch holds its room from start up to, but not including, end, so a batch may start in a room in the minute the previous batch there ends. Write assign_rooms(batches) in rooms.py. It returns a list with one room number for each batch, in the order the batches are given, such that:

  • no two batches in the same room overlap;
  • the rooms are numbered 1, 2, 3 and so on, with no number skipped;
  • as few rooms are used as any plan could manage.

Any plan with these properties passes: the tests check your plan rather than compare it with one answer.

assign_rooms([(480, 600), (540, 630), (600, 660)])  # for example [1, 2, 1]: 2 rooms
assign_rooms([(600, 660), (660, 720)])              # [1, 1]: back to back fits in one room
assign_rooms([])                                    # []
  • 0 ≤ start < end ≤ 10⁹; batches may repeat, sit inside one another or touch.
  • A list holds up to 100,000 batches. Leave the caller's list as it was.

The sample tests check these cases, identical batches, batches inside batches, a long batch beside many short ones, 100,000 batches, and 300 random lists against a search of their own that tries every plan.

Starter code · rooms.py

def assign_rooms(batches):
    """A room number (1, 2, 3 ...) for every batch, in the order given, using the fewest rooms."""
    # Replace this line with your code.
    return None
The sample tests · test_rooms.py
import random

from rooms import assign_rooms


def rooms_needed(batches):
    """The most batches running in one minute: no plan can use fewer rooms."""
    events = sorted([(end, -1) for _, end in batches] + [(start, 1) for start, _ in batches])
    running = peak = 0
    for _, change in events:
        running += change
        peak = max(peak, running)
    return peak


def check(batches, room):
    """The plan has one room per batch, numbers 1 to k with none skipped, no clash, and the fewest rooms."""
    assert isinstance(room, list) and len(room) == len(batches), f"one room number per batch, got {room!r}"
    used = sorted(set(room))
    assert used == list(range(1, len(used) + 1)), f"rooms must be numbered 1, 2, 3 ... with none skipped: {used}"
    by_room = {}
    for (start, end), r in zip(batches, room):
        by_room.setdefault(r, []).append((start, end))
    for r, held in by_room.items():
        held.sort()
        for (s1, e1), (s2, e2) in zip(held, held[1:]):
            assert e1 <= s2, f"room {r} holds ({s1}, {e1}) and ({s2}, {e2}), which overlap"
    assert len(used) == rooms_needed(batches), f"{len(used)} rooms, but {rooms_needed(batches)} are enough"


def by_search(batches):
    """A search the tests trust: the fewest rooms over every plan (small lists only)."""
    def place(i, ends, k):
        if i == len(batches):
            return True
        start, end = batches[i]
        for r in range(len(ends)):
            if all(e <= start or end <= s for s, e in ends[r]):
                ends[r].append((start, end))
                if place(i + 1, ends, k):
                    return True
                ends[r].pop()
        if len(ends) < k:
            ends.append([(start, end)])
            if place(i + 1, ends, k):
                return True
            ends.pop()
        return False

    return next(k for k in range(len(batches) + 1) if place(0, [], k))


def test_prompt_examples():
    """the three examples of the prompt"""
    for batches in ([(480, 600), (540, 630), (600, 660)], [(600, 660), (660, 720)], []):
        check(batches, assign_rooms(batches))


def test_saturday():
    """eight batches of one Saturday, which need 3 rooms"""
    saturday = [(480, 600), (540, 630), (600, 660), (615, 735), (660, 720), (690, 810), (720, 780), (780, 870)]
    room = assign_rooms(saturday)
    check(saturday, room)
    assert max(room) == 3


def test_identical_and_nested():
    """copies of one batch each need a room; a batch inside another shares no room with it"""
    check([(600, 660)] * 4, assign_rooms([(600, 660)] * 4))
    check([(0, 100), (10, 20), (30, 40), (20, 30)], assign_rooms([(0, 100), (10, 20), (30, 40), (20, 30)]))


def test_long_batch_beside_short_ones():
    """one long batch and twenty short ones back to back: 2 rooms"""
    batches = [(10 * k, 10 * k + 10) for k in range(20)] + [(0, 200)]
    room = assign_rooms(batches)
    check(batches, room)
    assert max(room) == 2


def test_input_unchanged():
    """the caller's list keeps its order"""
    batches = [(660, 720), (480, 600), (600, 660)]
    assign_rooms(batches)
    assert batches == [(660, 720), (480, 600), (600, 660)]


def test_hundred_thousand_batches():
    """100,000 batches with times up to 10**9"""
    rng = random.Random(4)
    batches = []
    for _ in range(100_000):
        start = rng.randrange(10**9 - 10**6)
        batches.append((start, start + rng.randint(1, 10**5)))
    check(batches, assign_rooms(batches))


def test_against_search():
    """300 random lists against the fewest rooms over every plan"""
    rng = random.Random(23)
    for _ in range(300):
        batches = []
        for _ in range(rng.randint(1, 7)):
            start = rng.randrange(10)
            batches.append((start, rng.randint(start + 1, 10)))
        room = assign_rooms(batches)
        check(batches, room)
        assert max(room) == by_search(batches), f"assign_rooms({batches})"
A hint

Take the batches in order of start. When a batch arrives, every room either is free by its start or still holds a batch. Which room is the one to check first, and what does it mean for every other room if even that one is still busy? Try it on the first example by hand: which batch opens room 2, and which batch reuses room 1?

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Gantt Chart Maker Draw your room plan as a chart with one row per room, and check by eye that no two batches in a row overlap.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 In which order does the one-room scan look at the batches?

    Choose one answer.

    Show the answer to question 1

    Answer: By end, earliest first

    The batch that ends first leaves the room free soonest, and any best plan starts with a batch that ends no earlier, so the scan sorts by end. Start order can keep a long batch that starts first and blocks several short ones, and shortest first can keep a short batch that overlaps two others.

  2. Question 2 of 4 Every batch must run. How many rooms do the batches (0, 4), (1, 3), (2, 6), (3, 5) and (5, 7) need?

    Type a number.

    Show the answer to question 2

    Answer: 3 rooms

    At minute 2, the batches (0, 4), (1, 3) and (2, 6) all run, so no plan can use fewer than three rooms. The room scan uses exactly three: (0, 4) opens room 1, (1, 3) room 2 and (2, 6) room 3; (3, 5) reuses room 2, which is free at 3, and (5, 7) reuses room 1, free at 4.

  3. Question 3 of 4 A batch starts at 11:00, and the room at the top of the heap is free only at 11:20. What does the scan do?

    Choose one answer.

    Show the answer to question 3

    Answer: Opens a new room, because every other room is free even later

    The top of the heap is the room that frees earliest. If even that room is busy at 11:00, every room is, so the batch needs a new one, and searching the others would find nothing. The batches never move: their times are fixed by the requests.

  4. Question 4 of 4 Which statements are true?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • With fees per batch, keeping the batches that end first can miss the best total
    • The fewest rooms equal the most batches running at one moment

    The busiest moment sets a floor that the room scan meets. With fees, a short cheap batch that ends first can block a dearer one, so a greedy rule can lose money. The one-room scan needs the order of end, not start. Batches are half-open, so a batch that ends at 10:00 frees its room for one that starts at 10:00.

Why both scans are optimal, checked by exhaustive search Pro: opens with a Pro, Premium or Ultimate pass

Each scan has a short proof, and each proof rests on one comparison that a careless version gets wrong. A stress test then sets both scans against exhaustive search on 2,000 random lists.

In this part:

  • Earliest finish stays ahead of every plan: the proof
  • The room scan uses exactly the floor: the proof
  • Checked against exhaustive search on 2,000 random lists

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Both scans in five languages, run on 100,000 requests Pro: opens with a Pro, Premium or Ultimate pass

The same two functions in Python, JavaScript, Java, C++ and Go, each run on the Saturday batches, the two small cases and a year of 100,000 random requests. Only the heap changes shape from one language to the next.

In this part:

  • The code, and what each version printed
  • Five heaps compared
  • Positions, not batches, are sorted
  • What the year costs, in numbers

2 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Rules that fail, and fees that need dynamic programming Pro: opens with a Pro, Premium or Ultimate pass

Ten rules are set against exhaustive search until each one fails or survives, and then batches get fees, which defeat every greedy rule tried here. The dynamic programme that replaces them runs on 50,000 requests.

In this part:

  • Ten rules and the smallest lists that break them
  • Fees per batch: three greedy rules fail
  • The dynamic programme, step by step
  • Edge cases and common bugs
  • A practice set with sample answers

2 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Activity selection keeps the activity that finishes first. Would keeping the one that starts last, working back from the end of the day, also give the most activities?

    Yes. Run the timetable on a clock that goes backwards: every start becomes an end and every end a start, and two activities overlap on the mirrored clock exactly when they overlap on the real one. The activity that starts last on the real clock is the one that finishes first on the mirrored clock, so latest start first is earliest finish first in a mirror, and the same stays-ahead proof applies. Sort by start, latest first, and keep each activity that ends at or before the start of the last one kept. The two rules may pick different activities but always keep the same number; on the lesson's Saturday both keep five batches. Earliest start first is the rule that fails, because a long activity that starts early can block several short ones.

  2. Mid to senior

    To give every batch a room, a teammate sorts the batches by end time and puts each one into the lowest-numbered room that is free for it. Does that always use the fewest rooms? Prove it or break it, and say what you would ship instead.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Each room needs 15 minutes of cleaning after a batch before the next one can start there. How do the most-batches scan and the fewest-rooms scan change, and do their proofs still hold?

    Model answer, Pro. First step and follow-ups

  4. Fresher to senior

    Another team's service returns a room plan for 100,000 batches. How do you check in O(n log n), without trusting their code, that the plan is valid and uses the fewest rooms? Which inputs would you send it first?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Activity selection: sort by finish and keep each batch that starts once the last kept one has ended.
  • Interval partitioning: sort by start, then reuse the room that frees earliest or open a new one.
  • The rooms used equal the most batches running at one moment, and no plan can use fewer.
  • With fees per batch, the greedy rules fail and dynamic programming takes over.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.