Fewest bookings to cancel so none overlap
Each booking is a pair [start, end] with start < end. Two bookings
clash if they share any time; bookings that only touch, like [1, 2] and [2, 3], do
not clash. Return the smallest number of bookings to cancel so that none of the remaining ones clash.
Examples
| Input | Output | Why |
|---|---|---|
bookings = [[1, 2], [2, 3], [3, 4], [1, 3]] | 1 | Cancel [1, 3]; the other three run back to back. |
bookings = [[1, 2], [1, 2], [1, 2]] | 2 | Only one copy can stay. |
bookings = [[1, 10], [2, 3], [4, 5], [6, 7]] | 1 | Cancelling the long one keeps three. |
Constraints
- 1 ≤ n ≤ 10⁵
- −5·10⁴ ≤ start < end ≤ 5·10⁴
Hints
Hint 1
Turn it round: cancelling the fewest is the same as keeping the most. Which booking should you keep first?
Hint 2
Of all the bookings, keep the one that finishes earliest. Why can that choice never be worse than any other?
Hint 3
Sort by end time. Keep a booking if it starts no earlier than the end of the last one you kept; otherwise cancel it.
Where people lose marks
- Sorting by start time: a long booking that starts first crowds out several short ones.
- Treating touching bookings as clashing, which cancels one booking too many.
- Keeping the shortest bookings first: a short booking in the middle can clash with two that would otherwise fit.
Dry run on [[1,10],[2,3],[4,5],[3,6],[6,7]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 6bookings, sorted by end2–304–513–626–731–104cancelled0
Sort by end time: 2–3, 4–5, 3–6, 6–7, 1–10. Nothing is kept yet.
-
Step 2 / 6bookings, sorted by end2–304–513–626–731–104last kept ends3cancelled0
2–3 starts at or after the beginning, the end of the last kept booking. Keep it.
-
Step 3 / 6bookings, sorted by end2–304–513–626–731–104last kept ends5cancelled0
4–5 starts at or after 3, the end of the last kept booking. Keep it.
-
Step 4 / 6bookings, sorted by end2–304–513–626–731–104last kept ends5cancelled1
3–6 starts before 5, while the last kept booking is still running. Cancel it.
-
Step 5 / 6bookings, sorted by end2–304–513–626–731–104last kept ends7cancelled1
6–7 starts at or after 5, the end of the last kept booking. Keep it.
-
Step 6 / 6bookings, sorted by end2–304–513–626–731–104last kept ends7cancelled2
1–10 starts before 7, while the last kept booking is still running. Cancel it. 2 cancelled, 3 kept.
How to think about it
Cancelling the fewest bookings is the same as keeping the largest set that doesn't clash. The greedy choice is the booking that ends earliest.
Why it's safe: take any best set of kept bookings and look at its first one. Swap it for the booking with the earliest end overall. That booking ends no later, so it clashes with nothing the first one didn't clash with, and the set is still valid and just as large. Repeat the argument on what's left.
- Sort by end time.
- Walk through: if a booking starts at or after the end of the last kept one, keep it; otherwise cancel it.
O(n log n) for the sort, then one pass.
Sorting by start time and keeping the earliest-starting booking fails: one long booking that starts first would block several short ones. Sort by end.
Complexity. Time O(n log n) · Space O(n) for the sorted copy
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def min_removals(bookings):
cancelled = 0
last_end = float('-inf')
for s, e in sorted(bookings, key=lambda b: b[1]): # by end time
if s >= last_end: # fits after the last kept booking: keep it
last_end = e
else: # clashes: cancel it
cancelled += 1
return cancelled#include <vector>
#include <algorithm>
#include <climits>
int solve(std::vector<std::vector<int>> b) {
std::sort(b.begin(), b.end(), [](const std::vector<int>& p, const std::vector<int>& q) { return p[1] < q[1]; });
int cancelled = 0;
long long lastEnd = LLONG_MIN;
for (const auto& x : b) {
if (x[0] >= lastEnd) lastEnd = x[1]; // fits: keep it
else ++cancelled; // clashes: cancel it
}
return cancelled;
}class Solution {
public int minRemovals(int[][] bookings) {
int[][] b = bookings.clone();
java.util.Arrays.sort(b, (p, q) -> Integer.compare(p[1], q[1])); // by end time
int cancelled = 0;
long lastEnd = Long.MIN_VALUE;
for (int[] x : b) {
if (x[0] >= lastEnd) lastEnd = x[1]; // fits: keep it
else cancelled++; // clashes: cancel it
}
return cancelled;
}
}function minRemovals(bookings) {
const b = bookings.map(x => x.slice()).sort((p, q) => p[1] - q[1]); // by end time
let cancelled = 0, lastEnd = -Infinity;
for (const [s, e] of b) {
if (s >= lastEnd) lastEnd = e; // fits: keep it
else cancelled++; // clashes: cancel it
}
return cancelled;
}fn solve(bookings: &[[i64; 2]]) -> i64 {
let mut b = bookings.to_vec();
b.sort_by_key(|x| x[1]); // by end time
let mut cancelled = 0;
let mut last_end = i64::MIN;
for &[s, e] in &b {
if s >= last_end {
last_end = e; // fits: keep it
} else {
cancelled += 1; // clashes: cancel it
}
}
cancelled
}import Data.List (sortOn)
-- Sort by end time; keep whatever fits after the last kept booking, count the rest.
solve :: [[Int]] -> Int
solve bookings = go minBound (sortOn (!! 1) bookings)
where
go _ [] = 0
go lastEnd ([s, e] : rest)
| s >= lastEnd = go e rest
| otherwise = 1 + go lastEnd rest
Variants to try
- Each booking has a value and you want the most valuable clash-free set: the greedy stops working, and it becomes dynamic programming (the next problem).
- Return which bookings to cancel: record the indices of the ones skipped.
- Shoot every balloon [start, end] with the fewest vertical arrows: the same sort by end, counting the kept ones.
Next problems
Total time covered by a set of bookings
Overlapping bookings must not be counted twice. Sort by start time and one pass merges them into blocks.
Open the problem → Medium18 minFewest meeting rooms for a day of meetings
The number of rooms needed is the most meetings that are ever running at once. Sort the starts and the ends and walk through them together.
Open the problem → Hard30 minMost profit from jobs that do not overlap
Each job has a value, so ending earliest is no longer safe. Sort by end time; each job either joins the best schedule ending before it starts, or is skipped.
Open the problem →Write a solution and run it against the real test table.
Keep going
Total time covered by a set of bookings
Overlapping bookings must not be counted twice. Sort by start time and one pass merges them into blocks.
Open the problem → Medium18 minFewest meeting rooms for a day of meetings
The number of rooms needed is the most meetings that are ever running at once. Sort the starts and the ends and walk through them together.
Open the problem → Hard30 minMost profit from jobs that do not overlap
Each job has a value, so ending earliest is no longer safe. Sort by end time; each job either joins the best schedule ending before it starts, or is skipped.
Open the problem → IntervalsThe pattern behind it
Sort by start to merge and count overlaps; sort by end to choose. One sorted pass replaces checking every pair.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.