A clear explanation of simulating an exam room by maintaining occupied seats in sorted order.
Problem Restatement
We need to design an ExamRoom class.
There are n seats in one row, labeled:
0, 1, 2, ..., n - 1When a student enters, they choose the seat that maximizes the distance to the closest occupied seat.
If several seats give the same distance, the student chooses the smallest seat number.
If the room is empty, the student sits at seat 0.
We need to support two operations:
| Operation | Meaning |
|---|---|
seat() | Choose a seat for the next student and return its index |
leave(p) | Mark seat p as empty |
For leave(p), the problem guarantees that a student is currently sitting at seat p.
Input and Output
| Item | Meaning |
|---|---|
| Constructor input | n, the number of seats |
seat() output | The chosen seat index |
leave(p) output | Nothing |
| Main rule | Choose the seat farthest from the closest occupied seat |
| Tie rule | Choose the smallest seat index |
Class shape:
class ExamRoom:
def __init__(self, n: int):
...
def seat(self) -> int:
...
def leave(self, p: int) -> None:
...Examples
Example:
examRoom = ExamRoom(10)
examRoom.seat() # returns 0
examRoom.seat() # returns 9
examRoom.seat() # returns 4
examRoom.seat() # returns 2
examRoom.leave(4)
examRoom.seat() # returns 5Walk through it.
At first, the room is empty, so the first student sits at 0.
Occupied seats:
[0]The next student wants to be farthest from seat 0, so they sit at 9.
Occupied seats:
[0, 9]The next student sits in the middle, at 4.
Occupied seats:
[0, 4, 9]The next student compares the gaps:
| Gap | Best seat | Distance to closest student |
|---|---|---|
Between 0 and 4 | 2 | 2 |
Between 4 and 9 | 6 | 2 |
Both give distance 2, so choose the smaller seat index, 2.
Occupied seats:
[0, 2, 4, 9]Then seat 4 leaves.
Occupied seats:
[0, 2, 9]The best open seat is now 5, between 2 and 9.
So the next seat() returns:
5First Thought: Track Occupied Seats
The main thing we need to know is where students are currently sitting.
If we keep the occupied seats in sorted order, then every possible best seat comes from one of three places:
- Seat
0, before the first occupied seat - A midpoint between two adjacent occupied seats
- Seat
n - 1, after the last occupied seat
So for each seat() call, we can scan the sorted occupied seats and compute the best candidate.
This is simple and reliable.
In Python, we can store occupied seats in a sorted list and use bisect.insort to insert new seats while keeping the list sorted.
Key Insight
Suppose two adjacent occupied seats are:
left
rightThe best seat between them is the midpoint:
(left + right) // 2The distance to the closest occupied seat is:
(right - left) // 2For the left edge, before the first occupied seat, the best candidate is seat 0.
Its distance is:
first_occupied - 0For the right edge, after the last occupied seat, the best candidate is seat n - 1.
Its distance is:
(n - 1) - last_occupiedWe choose the candidate with the largest distance.
If distances tie, we keep the smaller seat index.
Scanning from left to right helps with ties, because smaller candidates appear earlier.
Algorithm
Maintain:
self.studentsas a sorted list of occupied seats.
For seat():
- If no seats are occupied:
- Insert
0 - Return
0
- Insert
- Start with candidate seat
0. - Its distance is the distance to the first occupied seat.
- For every adjacent pair of occupied seats:
- Compute the midpoint.
- Compute its distance to the closest occupied seat.
- If this distance is larger than the current best, update the answer.
- Check seat
n - 1. - Insert the chosen seat into the sorted list.
- Return it.
For leave(p):
- Remove
pfrom the sorted list.
Correctness
When the room is empty, the rule says the student must sit at seat 0, and the algorithm does exactly that.
Now suppose the room is not empty.
Any empty seat belongs to one of three regions: before the first occupied seat, between two adjacent occupied seats, or after the last occupied seat.
In the left region, seat 0 is farthest from the first occupied seat, so it is the best candidate for that region.
In the right region, seat n - 1 is farthest from the last occupied seat, so it is the best candidate for that region.
Between two adjacent occupied seats left and right, the seat that maximizes the minimum distance to both sides is the midpoint (left + right) // 2. If there are two central choices, the smaller index is correct because ties must choose the smaller seat.
Therefore, checking these candidates covers the best possible seat in every region.
The algorithm chooses the candidate with the largest distance. It only updates when it finds a strictly larger distance, so when distances tie, the earlier smaller candidate remains selected.
Thus, seat() always returns the seat required by the problem.
The leave(p) operation removes an occupied seat. Since the problem guarantees p is occupied, removing it restores the correct occupied-seat set.
Complexity
Let k be the number of currently occupied seats.
| Operation | Time | Why |
|---|---|---|
seat() | O(k) scan plus O(k) insertion | We scan occupied seats and insert into a sorted list |
leave(p) | O(k) | Removing from a Python list may shift elements |
| Space | O(k) | We store occupied seats |
For the LeetCode constraints, this sorted-list approach is accepted and much simpler than an interval tree.
Implementation
from bisect import insort
class ExamRoom:
def __init__(self, n: int):
self.n = n
self.students = []
def seat(self) -> int:
if not self.students:
self.students.append(0)
return 0
best_seat = 0
best_dist = self.students[0]
for i in range(1, len(self.students)):
left = self.students[i - 1]
right = self.students[i]
seat = (left + right) // 2
dist = min(seat - left, right - seat)
if dist > best_dist:
best_dist = dist
best_seat = seat
last_seat = self.n - 1
right_dist = last_seat - self.students[-1]
if right_dist > best_dist:
best_seat = last_seat
insort(self.students, best_seat)
return best_seat
def leave(self, p: int) -> None:
self.students.remove(p)Code Explanation
The constructor stores the number of seats and starts with no occupied seats:
def __init__(self, n: int):
self.n = n
self.students = []When the first student enters, the room is empty:
if not self.students:
self.students.append(0)
return 0The first candidate is seat 0:
best_seat = 0
best_dist = self.students[0]If the first occupied seat is at index 7, then seat 0 has distance 7.
Next, we inspect every gap between adjacent occupied seats:
for i in range(1, len(self.students)):
left = self.students[i - 1]
right = self.students[i]The best seat in this gap is the midpoint:
seat = (left + right) // 2Its distance to the closest occupied seat is:
dist = min(seat - left, right - seat)If this distance is better than the best one found so far, we update:
if dist > best_dist:
best_dist = dist
best_seat = seatWe use > rather than >=.
That preserves the smaller seat index in a tie.
After checking middle gaps, we check the right edge:
last_seat = self.n - 1
right_dist = last_seat - self.students[-1]If the right edge gives a strictly better distance, choose it:
if right_dist > best_dist:
best_seat = last_seatFinally, insert the selected seat while keeping the list sorted:
insort(self.students, best_seat)For leaving:
self.students.remove(p)This removes the occupied seat p.
Testing
def test_exam_room():
room = ExamRoom(10)
assert room.seat() == 0
assert room.seat() == 9
assert room.seat() == 4
assert room.seat() == 2
room.leave(4)
assert room.seat() == 5
room = ExamRoom(5)
assert room.seat() == 0
assert room.seat() == 4
assert room.seat() == 2
assert room.seat() == 1
assert room.seat() == 3
room.leave(0)
assert room.seat() == 0
room = ExamRoom(3)
assert room.seat() == 0
assert room.seat() == 2
room.leave(0)
assert room.seat() == 0
print("all tests passed")
test_exam_room()Test meaning:
| Test | Why |
|---|---|
| Official-style sequence | Checks seat selection and leave |
| Fill all seats | Checks repeated midpoint and tie behavior |
| Leave first seat | Checks left edge candidate |
| Small room | Checks boundary behavior |