LeetCode 2286 - Booking Concert Tickets in Groups
The problem asks us to design a ticket reservation system for a concert hall. The hall contains n rows, and each row contains exactly m seats. Seats in every row are numbered from left to right starting at 0.
Difficulty: 🔴 Hard
Topics: Binary Search, Design, Binary Indexed Tree, Segment Tree
Solution
Problem Understanding
The problem asks us to design a ticket reservation system for a concert hall. The hall contains n rows, and each row contains exactly m seats. Seats in every row are numbered from left to right starting at 0.
We must support two different operations:
The first operation, gather(k, maxRow), tries to reserve k consecutive seats in the same row. The row number must be at most maxRow. Among all valid choices, we must always choose the smallest possible row index. Within that row, we must choose the leftmost available block of seats. If no row from 0 through maxRow has k consecutive empty seats, we return an empty array.
The second operation, scatter(k, maxRow), is less restrictive. The k people do not need to sit together. We only need to ensure that there are at least k total empty seats across rows 0 through maxRow. If possible, seats are allocated greedily using the smallest row numbers first, and within each row, the smallest seat numbers first. The method returns true if allocation succeeds and false otherwise.
A key observation is that seats are always filled from left to right within each row. We never create gaps inside a row. This property becomes extremely important because it means we only need to track how many seats have already been used in each row.
The constraints are large:
n <= 5 * 10^4- At most
5 * 10^4operations mandkcan be as large as10^9
A naive simulation that checks every seat individually would be far too slow. Even iterating through all rows repeatedly can become expensive if done carelessly.
The main challenge is efficiently answering two questions repeatedly:
- Does some row up to
maxRowhave at leastkconsecutive empty seats? - How many total empty seats exist up to
maxRow?
These requirements strongly suggest a segment tree, because we need efficient range queries and updates.
Important edge cases include:
- A row may become completely full.
gathermust always choose the smallest valid row.scattermay partially fill several rows.kcan exceed the capacity of any single row, makinggatherimmediately impossible.- After many operations, the hall may be almost full, so efficient skipping of exhausted rows is necessary.
Approaches
Brute Force Approach
The most straightforward solution is to explicitly track how many seats remain in every row.
For gather(k, maxRow), we scan rows from 0 to maxRow and check whether the row has at least k consecutive seats available. Since seats are always allocated from left to right, this simply means checking whether the remaining capacity is at least k.
For scatter(k, maxRow), we first count how many seats remain across rows 0 through maxRow. If enough seats exist, we iterate row by row and consume seats until all k spectators are seated.
This approach is logically correct because it directly follows the problem rules. However, the performance is poor. In the worst case, each operation may scan up to O(n) rows, and there can be 5 * 10^4 operations. That leads to roughly 2.5 * 10^9 row inspections in the worst case, which is far too slow.
Optimal Approach
The key insight is that we repeatedly need two types of aggregated information over row ranges:
- The maximum number of remaining seats in any row, for
gather - The total number of remaining seats across rows, for
scatter
A segment tree is ideal because it supports both range aggregation and point updates efficiently.
We maintain:
maxTree[node], the maximum remaining seats in that segmentsumTree[node], the total remaining seats in that segment
Each row initially contains m available seats.
For gather, we use the max segment tree to locate the leftmost row with at least k available seats.
For scatter, we use the sum segment tree to determine whether enough total seats exist. If so, we greedily consume seats from the smallest rows.
Each update only affects one row, so segment tree updates take O(log n) time.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) per operation | O(n) | Scans rows repeatedly |
| Optimal | O(log n) for gather, O(log n + affected rows) for scatter | O(n) | Uses segment tree for efficient aggregation |
Algorithm Walkthrough
Data Structures
We maintain three important pieces of state:
used[row], the number of seats already occupied in each rowmaxTree, storing the maximum remaining seats in each segmentsumTree, storing the total remaining seats in each segment
Initially:
used[row] = 0- every row has
mavailable seats
Gather Operation
- First, check whether any row in range
[0, maxRow]has at leastkfree seats. - We query the segment tree maximum over that range. If the maximum is less than
k, gathering is impossible. - Otherwise, we recursively search the segment tree for the leftmost row whose remaining capacity is at least
k. - Once the row is found, the starting seat index is simply
used[row], because seats are always allocated from left to right. - We increase
used[row]byk. - We update the segment tree values for that row:
- remaining seats become
m - used[row] - both max and sum values are refreshed upward through the tree
- Return
[row, startingSeat].
Scatter Operation
- First, compute the total remaining seats in rows
[0, maxRow]. - If the total is smaller than
k, returnfalse. - Otherwise, we begin allocating seats greedily from the smallest available row.
- For each row:
- determine how many seats remain
- allocate as many as possible
- update
used[row] - update the segment tree
- Continue until all
kspectators are seated. - Return
true.
Why it works
The algorithm works because the segment tree always maintains accurate aggregated information for every row range.
The maxTree invariant guarantees that we can efficiently determine whether some row can fit a consecutive block of size k, and locate the leftmost such row.
The sumTree invariant guarantees that we can correctly determine whether enough total seats exist for scatter.
Because seats are always allocated from left to right and rows are processed in increasing order, the implementation exactly matches the required tie breaking rules.
Python Solution
from typing import List
class BookMyShow:
def __init__(self, n: int, m: int):
self.n = n
self.m = m
self.used = [0] * n
size = 4 * n
self.max_tree = [0] * size
self.sum_tree = [0] * size
self._build(1, 0, n - 1)
self.scatter_row = 0
def _build(self, node: int, left: int, right: int) -> None:
if left == right:
self.max_tree[node] = self.m
self.sum_tree[node] = self.m
return
mid = (left + right) // 2
self._build(node * 2, left, mid)
self._build(node * 2 + 1, mid + 1, right)
self.max_tree[node] = self.m
self.sum_tree[node] = (right - left + 1) * self.m
def _update(self, node: int, left: int, right: int, idx: int) -> None:
if left == right:
remaining = self.m - self.used[idx]
self.max_tree[node] = remaining
self.sum_tree[node] = remaining
return
mid = (left + right) // 2
if idx <= mid:
self._update(node * 2, left, mid, idx)
else:
self._update(node * 2 + 1, mid + 1, right, idx)
self.max_tree[node] = max(
self.max_tree[node * 2],
self.max_tree[node * 2 + 1]
)
self.sum_tree[node] = (
self.sum_tree[node * 2]
+ self.sum_tree[node * 2 + 1]
)
def _query_max(
self,
node: int,
left: int,
right: int,
ql: int,
qr: int
) -> int:
if ql <= left and right <= qr:
return self.max_tree[node]
mid = (left + right) // 2
result = 0
if ql <= mid:
result = max(
result,
self._query_max(node * 2, left, mid, ql, qr)
)
if qr > mid:
result = max(
result,
self._query_max(node * 2 + 1, mid + 1, right, ql, qr)
)
return result
def _query_sum(
self,
node: int,
left: int,
right: int,
ql: int,
qr: int
) -> int:
if ql <= left and right <= qr:
return self.sum_tree[node]
mid = (left + right) // 2
total = 0
if ql <= mid:
total += self._query_sum(
node * 2,
left,
mid,
ql,
qr
)
if qr > mid:
total += self._query_sum(
node * 2 + 1,
mid + 1,
right,
ql,
qr
)
return total
def _find_row(
self,
node: int,
left: int,
right: int,
max_row: int,
k: int
) -> int:
if left == right:
return left
mid = (left + right) // 2
if (
left <= max_row
and self.max_tree[node * 2] >= k
):
return self._find_row(
node * 2,
left,
mid,
max_row,
k
)
return self._find_row(
node * 2 + 1,
mid + 1,
right,
max_row,
k
)
def gather(self, k: int, maxRow: int) -> List[int]:
if self._query_max(1, 0, self.n - 1, 0, maxRow) < k:
return []
row = self._find_row(1, 0, self.n - 1, maxRow, k)
seat = self.used[row]
self.used[row] += k
self._update(1, 0, self.n - 1, row)
return [row, seat]
def scatter(self, k: int, maxRow: int) -> bool:
available = self._query_sum(1, 0, self.n - 1, 0, maxRow)
if available < k:
return False
row = self.scatter_row
while k > 0 and row <= maxRow:
remaining = self.m - self.used[row]
if remaining == 0:
row += 1
continue
take = min(k, remaining)
self.used[row] += take
self._update(1, 0, self.n - 1, row)
k -= take
if self.used[row] == self.m:
row += 1
self.scatter_row = row
return True
The implementation begins by constructing a segment tree where every row initially has m available seats.
The _build method initializes the tree. Each leaf corresponds to one row. Internal nodes aggregate information from children.
The _update method refreshes one row after seats are allocated. Since only one row changes at a time, updates propagate upward efficiently.
The _query_max method determines whether some row in a range can satisfy a gather request.
The _query_sum method computes total remaining capacity for scatter.
The _find_row method performs a binary search over the segment tree structure itself. It always explores the left child first, guaranteeing that we find the smallest valid row.
The scatter_row pointer is an optimization. Once a row becomes completely full, future scatter operations never need to revisit it.
Go Solution
package main
type BookMyShow struct {
n int
m int
used []int
maxTree []int
sumTree []int64
scatterRow int
}
func Constructor(n int, m int) BookMyShow {
bms := BookMyShow{
n: n,
m: m,
used: make([]int, n),
maxTree: make([]int, 4*n),
sumTree: make([]int64, 4*n),
scatterRow: 0,
}
bms.build(1, 0, n-1)
return bms
}
func (this *BookMyShow) build(node int, left int, right int) {
if left == right {
this.maxTree[node] = this.m
this.sumTree[node] = int64(this.m)
return
}
mid := (left + right) / 2
this.build(node*2, left, mid)
this.build(node*2+1, mid+1, right)
this.maxTree[node] = this.m
this.sumTree[node] = int64(right-left+1) * int64(this.m)
}
func (this *BookMyShow) update(
node int,
left int,
right int,
idx int,
) {
if left == right {
remaining := this.m - this.used[idx]
this.maxTree[node] = remaining
this.sumTree[node] = int64(remaining)
return
}
mid := (left + right) / 2
if idx <= mid {
this.update(node*2, left, mid, idx)
} else {
this.update(node*2+1, mid+1, right, idx)
}
if this.maxTree[node*2] > this.maxTree[node*2+1] {
this.maxTree[node] = this.maxTree[node*2]
} else {
this.maxTree[node] = this.maxTree[node*2+1]
}
this.sumTree[node] =
this.sumTree[node*2] +
this.sumTree[node*2+1]
}
func (this *BookMyShow) queryMax(
node int,
left int,
right int,
ql int,
qr int,
) int {
if ql <= left && right <= qr {
return this.maxTree[node]
}
mid := (left + right) / 2
result := 0
if ql <= mid {
val := this.queryMax(node*2, left, mid, ql, qr)
if val > result {
result = val
}
}
if qr > mid {
val := this.queryMax(node*2+1, mid+1, right, ql, qr)
if val > result {
result = val
}
}
return result
}
func (this *BookMyShow) querySum(
node int,
left int,
right int,
ql int,
qr int,
) int64 {
if ql <= left && right <= qr {
return this.sumTree[node]
}
mid := (left + right) / 2
var total int64 = 0
if ql <= mid {
total += this.querySum(node*2, left, mid, ql, qr)
}
if qr > mid {
total += this.querySum(node*2+1, mid+1, right, ql, qr)
}
return total
}
func (this *BookMyShow) findRow(
node int,
left int,
right int,
maxRow int,
k int,
) int {
if left == right {
return left
}
mid := (left + right) / 2
if mid >= left &&
this.maxTree[node*2] >= k &&
left <= maxRow {
return this.findRow(
node*2,
left,
mid,
maxRow,
k,
)
}
return this.findRow(
node*2+1,
mid+1,
right,
maxRow,
k,
)
}
func (this *BookMyShow) Gather(
k int,
maxRow int,
) []int {
if this.queryMax(1, 0, this.n-1, 0, maxRow) < k {
return []int{}
}
row := this.findRow(1, 0, this.n-1, maxRow, k)
seat := this.used[row]
this.used[row] += k
this.update(1, 0, this.n-1, row)
return []int{row, seat}
}
func (this *BookMyShow) Scatter(
k int,
maxRow int,
) bool {
available := this.querySum(1, 0, this.n-1, 0, maxRow)
if available < int64(k) {
return false
}
row := this.scatterRow
for k > 0 && row <= maxRow {
remaining := this.m - this.used[row]
if remaining == 0 {
row++
continue
}
take := remaining
if k < take {
take = k
}
this.used[row] += take
this.update(1, 0, this.n-1, row)
k -= take
if this.used[row] == this.m {
row++
}
}
this.scatterRow = row
return true
}
The Go implementation mirrors the Python logic closely, but there are a few language specific details worth noting.
The total number of seats can exceed 32 bit integer range because n * m may be as large as 5 * 10^13. For this reason, sumTree uses int64.
Go does not have dynamic lists like Python, so empty results are returned as []int{}.
Methods are implemented with pointer receivers so updates modify the shared structure correctly.
Worked Examples
Example 1
Input:
BookMyShow(2, 5)
gather(4, 0)
gather(2, 0)
scatter(5, 1)
scatter(5, 1)
Initial state:
| Row | Used | Remaining |
|---|---|---|
| 0 | 0 | 5 |
| 1 | 0 | 5 |
Operation 1: gather(4, 0)
We search rows [0, 0].
Row 0 has 5 remaining seats, which is enough.
We allocate seats [0,1,2,3].
Updated state:
| Row | Used | Remaining |
|---|---|---|
| 0 | 4 | 1 |
| 1 | 0 | 5 |
Return:
[0, 0]
Operation 2: gather(2, 0)
We search rows [0,0].
Row 0 only has 1 remaining seat.
No valid row exists.
Return:
[]
State remains unchanged.
Operation 3: scatter(5, 1)
Total remaining seats:
- Row 0: 1
- Row 1: 5
Total = 6, so allocation succeeds.
We allocate:
- 1 seat in row 0
- 4 seats in row 1
Updated state:
| Row | Used | Remaining |
|---|---|---|
| 0 | 5 | 0 |
| 1 | 4 | 1 |
Return:
true
Operation 4: scatter(5, 1)
Total remaining seats:
- Row 0: 0
- Row 1: 1
Total = 1.
Not enough seats.
Return:
false
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(log n) for gather, amortized O(log n) for scatter | Segment tree queries and updates dominate |
| Space | O(n) | Segment tree and row tracking arrays |
The segment tree contains O(n) nodes.
Each gather operation performs:
- one max query
- one search down the tree
- one update
Each step costs O(log n).
For scatter, rows are only advanced forward because of the scatter_row pointer. Across all operations, each row becomes full at most once. Therefore, the total extra scanning work is amortized linear over the entire execution.
Test Cases
from typing import List
# Example case
bms = BookMyShow(2, 5)
assert bms.gather(4, 0) == [0, 0] # basic gather
assert bms.gather(2, 0) == [] # insufficient consecutive seats
assert bms.scatter(5, 1) is True # scatter across rows
assert bms.scatter(5, 1) is False # insufficient total seats
# Single row exact fit
bms = BookMyShow(1, 5)
assert bms.gather(5, 0) == [0, 0] # fill entire row
assert bms.gather(1, 0) == [] # row now full
# Scatter partially fills rows
bms = BookMyShow(3, 4)
assert bms.scatter(6, 2) is True # fills row 0 and part of row 1
assert bms.gather(3, 1) == [] # row 1 only has 2 seats left
assert bms.gather(2, 1) == [1, 2] # valid consecutive block
# Gather prefers smallest row
bms = BookMyShow(3, 5)
assert bms.gather(2, 2) == [0, 0]
assert bms.gather(2, 2) == [0, 2]
assert bms.gather(2, 2) == [1, 0] # row 0 no longer fits
# Scatter respects maxRow
bms = BookMyShow(3, 5)
assert bms.scatter(6, 0) is False # only row 0 available
# Large k impossible for gather
bms = BookMyShow(5, 10)
assert bms.gather(11, 4) == [] # row capacity exceeded
# Full exhaustion
bms = BookMyShow(2, 2)
assert bms.scatter(4, 1) is True # consume everything
assert bms.scatter(1, 1) is False # no seats remain
assert bms.gather(1, 1) == [] # no consecutive seat exists
# Scatter after gathers
bms = BookMyShow(2, 5)
assert bms.gather(3, 1) == [0, 0]
assert bms.scatter(4, 1) is True # uses row 0 remainder + row 1
assert bms.gather(2, 1) == [] # insufficient consecutive seats
| Test | Why |
|---|---|
| Example from prompt | Verifies correctness against official behavior |
| Single row exact fit | Tests complete exhaustion |
| Scatter partially fills rows | Ensures partial row handling works |
| Gather prefers smallest row | Verifies tie breaking rules |
| Scatter respects maxRow | Ensures row restriction is enforced |
| Large k impossible | Tests impossible gather requests |
| Full exhaustion | Verifies fully occupied hall behavior |
| Scatter after gathers | Tests mixed operation ordering |
Edge Cases
One important edge case occurs when k exceeds the capacity of every row. Since a gather request requires consecutive seats in one row, no allocation is possible even if many total seats remain across multiple rows. The implementation handles this naturally because the segment tree maximum query immediately determines that no row has enough remaining capacity.
Another tricky case happens when rows become completely full. A naive scatter implementation might repeatedly revisit exhausted rows, causing severe performance degradation. The solution avoids this problem using the scatter_row pointer, which permanently skips rows that are already full.
A third important edge case involves tie breaking. The problem requires always choosing the smallest possible row and then the smallest possible seat number. This can easily be implemented incorrectly if the search order is not carefully controlled. The segment tree search always explores the left child first, guaranteeing that the first valid row discovered is the smallest valid row. Since seats are allocated from left to right, the starting seat index is always minimal as well.