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.
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
What you’ll learn · Before you start
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
On this page
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⁹.
- The eight requests in the figure: one room holds five of them, and all eight run in three rooms.
[(600, 660), (660, 720)]: back to back, so one room holds both.- A single batch: one kept, in room 1.
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.
- Maths, 08:00 to 10:00: one room holds it; room 1.
- Physics, 09:00 to 10:30: left out with one room; room 2.
- Chemistry, 10:00 to 11:00: one room holds it; room 1.
- English, 10:15 to 12:15: left out with one room; room 3.
- Biology, 11:00 to 12:00: one room holds it; room 2.
- Coding, 11:30 to 13:30: left out with one room; room 1.
- Hindi, 12:00 to 13:00: one room holds it; room 2.
- 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:
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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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.
-
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?
-
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?
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
- Algorithm Design (Kleinberg and Tardos), lecture slides: Greedy Algorithms I (interval scheduling and interval partitioning) (Pearson Addison-Wesley (slides by Kevin Wayne, Princeton University))
- Algorithm Design (Kleinberg and Tardos), lecture slides: Dynamic Programming I (weighted interval scheduling) (Pearson Addison-Wesley (slides by Kevin Wayne, Princeton University))
- greedy algorithm (Dictionary of Algorithms and Data Structures) (National Institute of Standards and Technology (NIST))
- heapq: heap queue algorithm (Python Software Foundation)
- bisect: array bisection algorithm (Python Software Foundation)
- Sorting Techniques (key functions and sort stability) (Python Software Foundation)
- Java SE 25 API: PriorityQueue (Oracle)
- std::priority_queue (C++ reference) (cppreference.com)
- Go standard library: package container/heap (The Go Authors)
- ECMAScript Language Specification: Array.prototype.sort (Ecma International (TC39))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress