290 lines
16 KiB
YAML
290 lines
16 KiB
YAML
title: Booking Concert Tickets in Groups
|
|
slug: booking-concert-tickets-in-groups
|
|
difficulty: hard
|
|
leetcode_id: 2286
|
|
leetcode_url: https://leetcode.com/problems/booking-concert-tickets-in-groups/
|
|
categories:
|
|
- arrays
|
|
- binary-search
|
|
patterns:
|
|
- binary-search
|
|
|
|
function_signature: "class BookMyShow"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input:
|
|
operations: ["BookMyShow", "gather", "gather", "scatter", "scatter"]
|
|
args: [[2, 5], [4, 0], [2, 0], [5, 1], [5, 1]]
|
|
expected: [null, [0, 0], [], true, false]
|
|
hidden:
|
|
- input:
|
|
operations: ["BookMyShow", "gather", "scatter"]
|
|
args: [[1, 5], [5, 0], [1, 0]]
|
|
expected: [null, [0, 0], false]
|
|
- input:
|
|
operations: ["BookMyShow", "scatter", "scatter", "gather"]
|
|
args: [[2, 3], [3, 1], [3, 1], [2, 1]]
|
|
expected: [null, true, true, []]
|
|
- input:
|
|
operations: ["BookMyShow", "gather", "gather", "gather"]
|
|
args: [[3, 4], [2, 0], [2, 1], [2, 2]]
|
|
expected: [null, [0, 0], [0, 2], [1, 0]]
|
|
|
|
description: |
|
|
A concert hall has `n` rows numbered from `0` to `n - 1`, each with `m` seats, numbered from `0` to `m - 1`. You need to design a ticketing system that can allocate seats in the following cases:
|
|
|
|
- If a group of `k` spectators can sit **together** in a row.
|
|
- If **every** member of a group of `k` spectators can get a seat. They may or **may not** sit together.
|
|
|
|
Note that the spectators are very picky. Hence:
|
|
|
|
- They will book seats only if each member of their group can get a seat with row number **less than or equal** to `maxRow`. `maxRow` can **vary** from group to group.
|
|
- In case there are multiple rows to choose from, the row with the **smallest** number is chosen. If there are multiple seats to choose in the same row, the seat with the **smallest** number is chosen.
|
|
|
|
Implement the `BookMyShow` class:
|
|
|
|
- `BookMyShow(int n, int m)` Initializes the object with `n` as the number of rows and `m` as the number of seats per row.
|
|
- `int[] gather(int k, int maxRow)` Returns an array of length `2` denoting the row and seat number (respectively) of the **first seat** being allocated to the `k` members of the group, who must sit **together**. In other words, it returns the smallest possible `r` and `c` such that all `[c, c + k - 1]` seats are valid and empty in row `r`, and `r <= maxRow`. Returns `[]` if it is **not possible** to allocate seats to the group.
|
|
- `boolean scatter(int k, int maxRow)` Returns `true` if all `k` members of the group can be allocated seats in rows `0` to `maxRow`, who may or **may not** sit together. If the seats can be allocated, it allocates `k` seats to the group with the **smallest** row numbers, and the smallest possible seat numbers in each row. Otherwise, returns `false`.
|
|
|
|
constraints: |
|
|
- `1 <= n <= 5 * 10^4`
|
|
- `1 <= m, k <= 10^9`
|
|
- `0 <= maxRow <= n - 1`
|
|
- At most `5 * 10^4` calls **in total** will be made to `gather` and `scatter`.
|
|
|
|
examples:
|
|
- input: |
|
|
["BookMyShow", "gather", "gather", "scatter", "scatter"]
|
|
[[2, 5], [4, 0], [2, 0], [5, 1], [5, 1]]
|
|
output: "[null, [0, 0], [], true, false]"
|
|
explanation: |
|
|
BookMyShow bms = new BookMyShow(2, 5); // There are 2 rows with 5 seats each
|
|
bms.gather(4, 0); // return [0, 0] - The group books seats [0, 3] of row 0.
|
|
bms.gather(2, 0); // return [] - There is only 1 seat left in row 0, so it is not possible to book 2 consecutive seats.
|
|
bms.scatter(5, 1); // return True - The group books seat 4 of row 0 and seats [0, 3] of row 1.
|
|
bms.scatter(5, 1); // return False - There is only one seat left in the hall.
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're managing a concert venue where groups constantly request seats. Some groups need to sit together (like families), while others just need any available seats (like colleagues who don't mind being separated).
|
|
|
|
The naive approach of iterating through rows one-by-one for each request would be too slow with up to `5 * 10^4` operations and `5 * 10^4` rows. We need a data structure that can efficiently answer two types of queries:
|
|
|
|
1. **Find the first row** (within a range) that has at least `k` consecutive empty seats
|
|
2. **Calculate the total** available seats across a range of rows
|
|
|
|
A **Segment Tree** is the perfect fit here. It can answer range queries and perform updates in O(log n) time. We'll build a segment tree where each node stores:
|
|
|
|
- The **maximum available consecutive seats** in any row within its range (for `gather`)
|
|
- The **sum of available seats** across all rows in its range (for `scatter`)
|
|
|
|
Think of it like a hierarchical index: instead of checking every row, we can quickly narrow down to the relevant section by looking at summary statistics at each level.
|
|
|
|
approach: |
|
|
We use a **Segment Tree** with two values per node: `max_val` (maximum seats available in any single row) and `sum_val` (total seats available across all rows in the range).
|
|
|
|
**Step 1: Initialize the segment tree**
|
|
|
|
- Create arrays to store `max_val` and `sum_val` for each segment tree node
|
|
- Initially, every row has `m` seats available
|
|
- Build the tree bottom-up: leaf nodes represent individual rows, parent nodes aggregate their children
|
|
|
|
|
|
|
|
**Step 2: Implement `gather(k, maxRow)`**
|
|
|
|
- Use the segment tree to find the **leftmost row** in `[0, maxRow]` where `max_val >= k`
|
|
- Binary search through the tree: if the left child's max is sufficient, go left; otherwise, go right
|
|
- Once found, allocate `k` seats from that row (update the row's available count)
|
|
- Return `[row, first_seat_index]`
|
|
- If no row found, return `[]`
|
|
|
|
|
|
|
|
**Step 3: Implement `scatter(k, maxRow)`**
|
|
|
|
- Query the segment tree for the **sum** of available seats in `[0, maxRow]`
|
|
- If `sum < k`, return `false`
|
|
- Otherwise, greedily fill rows starting from the smallest index:
|
|
- Track a pointer to the first row that might have seats
|
|
- Fill each row as much as possible until `k` seats are allocated
|
|
- Update the segment tree after each row modification
|
|
- Return `true`
|
|
|
|
|
|
|
|
**Step 4: Segment tree operations**
|
|
|
|
- `update(row, new_val)`: Update a single row's available seats, then propagate changes up
|
|
- `query_max(l, r)`: Find the maximum available seats in any row within `[l, r]`
|
|
- `query_sum(l, r)`: Find the total available seats in `[l, r]`
|
|
- `find_first(k, maxRow)`: Binary search for the leftmost row with at least `k` seats
|
|
|
|
common_pitfalls:
|
|
- title: Using O(n) Linear Scan Per Query
|
|
description: |
|
|
A naive implementation might iterate through all rows for each `gather` or `scatter` call. With `n = 5 * 10^4` rows and `5 * 10^4` queries, this results in `O(n * q) = 2.5 * 10^9` operations, causing TLE.
|
|
|
|
The segment tree reduces each query to O(log n), giving us `O(q * log n)` total time.
|
|
wrong_approach: "Linear scan through all rows for each query"
|
|
correct_approach: "Segment tree with O(log n) queries"
|
|
|
|
- title: Integer Overflow in Sum Calculations
|
|
description: |
|
|
With `m` up to `10^9` and `n` up to `5 * 10^4`, the total seats can reach `5 * 10^13`, which exceeds the 32-bit integer limit (`~2 * 10^9`).
|
|
|
|
Always use 64-bit integers (Python handles this automatically, but in other languages use `long long` or similar).
|
|
wrong_approach: "Using 32-bit integers for seat counts"
|
|
correct_approach: "Use 64-bit integers for sum calculations"
|
|
|
|
- title: Not Tracking the First Non-Empty Row for Scatter
|
|
description: |
|
|
In `scatter`, if you always start from row 0, you waste time iterating through already-full rows. After filling rows, keep track of the first row that might still have available seats.
|
|
|
|
This optimization is crucial because `scatter` might fill many rows across multiple calls, and re-checking full rows adds unnecessary overhead.
|
|
wrong_approach: "Always start scatter from row 0"
|
|
correct_approach: "Maintain a pointer to the first potentially non-empty row"
|
|
|
|
- title: Incorrect Binary Search in Segment Tree
|
|
description: |
|
|
When finding the leftmost row with sufficient seats, you must check the left subtree first. If the left subtree's maximum is sufficient, the answer is there. Only check the right subtree if the left cannot satisfy the requirement.
|
|
|
|
Getting this order wrong will return a valid row but not necessarily the **smallest** valid row number.
|
|
wrong_approach: "Not prioritizing the left subtree in binary search"
|
|
correct_approach: "Always check left child first, only go right if left is insufficient"
|
|
|
|
key_takeaways:
|
|
- "**Segment trees for range queries**: When you need to efficiently query and update ranges (max, sum, min), segment trees provide O(log n) operations"
|
|
- "**Dual-purpose nodes**: A single segment tree can track multiple aggregates (max and sum) by storing both values in each node"
|
|
- "**Binary search within segment tree**: Finding the leftmost element satisfying a condition can be done in O(log n) by binary searching through the tree structure"
|
|
- "**Design problems require thoughtful data structures**: The key insight is recognizing that a segment tree's properties match the query patterns perfectly"
|
|
|
|
time_complexity: "O(n + q * log n). Building the segment tree takes O(n), and each of the q operations takes O(log n) for queries and updates."
|
|
space_complexity: "O(n). The segment tree requires O(4n) = O(n) space for storing node values."
|
|
|
|
solutions:
|
|
- approach_name: Segment Tree
|
|
is_optimal: true
|
|
code: |
|
|
class BookMyShow:
|
|
def __init__(self, n: int, m: int):
|
|
self.n = n
|
|
self.m = m
|
|
# Segment tree arrays: 4*n size to handle all nodes
|
|
self.max_tree = [m] * (4 * n) # Max seats in any row in range
|
|
self.sum_tree = [m] * (4 * n) # Total seats in range
|
|
# Track first row that might have seats (optimization for scatter)
|
|
self.first_row = 0
|
|
# Build the segment tree
|
|
self._build(1, 0, n - 1)
|
|
|
|
def _build(self, node: int, start: int, end: int) -> None:
|
|
"""Build segment tree with initial values."""
|
|
if start == end:
|
|
# Leaf node: single row with m seats
|
|
self.max_tree[node] = self.m
|
|
self.sum_tree[node] = self.m
|
|
return
|
|
mid = (start + end) // 2
|
|
left, right = 2 * node, 2 * node + 1
|
|
self._build(left, start, mid)
|
|
self._build(right, mid + 1, end)
|
|
# Parent aggregates children
|
|
self.max_tree[node] = max(self.max_tree[left], self.max_tree[right])
|
|
self.sum_tree[node] = self.sum_tree[left] + self.sum_tree[right]
|
|
|
|
def _update(self, node: int, start: int, end: int, idx: int, val: int) -> None:
|
|
"""Update a single row's available seats."""
|
|
if start == end:
|
|
# Leaf node: set new value
|
|
self.max_tree[node] = val
|
|
self.sum_tree[node] = val
|
|
return
|
|
mid = (start + end) // 2
|
|
left, right = 2 * node, 2 * node + 1
|
|
if idx <= mid:
|
|
self._update(left, start, mid, idx, val)
|
|
else:
|
|
self._update(right, mid + 1, end, idx, val)
|
|
# Recalculate parent after child update
|
|
self.max_tree[node] = max(self.max_tree[left], self.max_tree[right])
|
|
self.sum_tree[node] = self.sum_tree[left] + self.sum_tree[right]
|
|
|
|
def _query_sum(self, node: int, start: int, end: int, l: int, r: int) -> int:
|
|
"""Query total available seats in range [l, r]."""
|
|
if r < start or end < l:
|
|
return 0 # Out of range
|
|
if l <= start and end <= r:
|
|
return self.sum_tree[node] # Fully within range
|
|
mid = (start + end) // 2
|
|
left, right = 2 * node, 2 * node + 1
|
|
return (self._query_sum(left, start, mid, l, r) +
|
|
self._query_sum(right, mid + 1, end, l, r))
|
|
|
|
def _find_first_row(self, node: int, start: int, end: int, k: int, max_row: int) -> int:
|
|
"""Find leftmost row in [start, min(end, max_row)] with at least k seats."""
|
|
if self.max_tree[node] < k or start > max_row:
|
|
return -1 # No valid row in this subtree
|
|
if start == end:
|
|
return start # Found the row
|
|
mid = (start + end) // 2
|
|
left, right = 2 * node, 2 * node + 1
|
|
# Check left subtree first (we want smallest row number)
|
|
left_result = self._find_first_row(left, start, mid, k, max_row)
|
|
if left_result != -1:
|
|
return left_result
|
|
# Only check right if left didn't have valid row
|
|
return self._find_first_row(right, mid + 1, end, k, max_row)
|
|
|
|
def _get_row_seats(self, node: int, start: int, end: int, idx: int) -> int:
|
|
"""Get current available seats in a specific row."""
|
|
if start == end:
|
|
return self.sum_tree[node]
|
|
mid = (start + end) // 2
|
|
if idx <= mid:
|
|
return self._get_row_seats(2 * node, start, mid, idx)
|
|
return self._get_row_seats(2 * node + 1, mid + 1, end, idx)
|
|
|
|
def gather(self, k: int, maxRow: int) -> list[int]:
|
|
"""Find k consecutive seats in a single row."""
|
|
# Find the first row with enough consecutive seats
|
|
row = self._find_first_row(1, 0, self.n - 1, k, maxRow)
|
|
if row == -1:
|
|
return []
|
|
# Get current seats in this row to find starting position
|
|
current_seats = self._get_row_seats(1, 0, self.n - 1, row)
|
|
# First empty seat is at position (m - current_seats)
|
|
first_seat = self.m - current_seats
|
|
# Update the row: k fewer seats available
|
|
self._update(1, 0, self.n - 1, row, current_seats - k)
|
|
return [row, first_seat]
|
|
|
|
def scatter(self, k: int, maxRow: int) -> bool:
|
|
"""Allocate k seats across multiple rows if possible."""
|
|
# Check if enough total seats exist in range [0, maxRow]
|
|
total = self._query_sum(1, 0, self.n - 1, 0, maxRow)
|
|
if total < k:
|
|
return False
|
|
# Greedily fill rows starting from first_row
|
|
remaining = k
|
|
row = self.first_row
|
|
while remaining > 0 and row <= maxRow:
|
|
seats = self._get_row_seats(1, 0, self.n - 1, row)
|
|
if seats > 0:
|
|
take = min(seats, remaining)
|
|
remaining -= take
|
|
self._update(1, 0, self.n - 1, row, seats - take)
|
|
# If row is now full, we can skip it next time
|
|
if seats - take == 0:
|
|
self.first_row = row + 1
|
|
row += 1
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(n + q * log n) — Building takes O(n), each query/update is O(log n).
|
|
|
|
**Space Complexity:** O(n) — Segment tree arrays use O(4n) space.
|
|
|
|
The segment tree stores two values per node: maximum seats in any row (for `gather`) and total seats (for `scatter`). The `gather` operation uses binary search within the tree to find the leftmost suitable row. The `scatter` operation checks total availability first, then greedily fills rows. The `first_row` pointer optimizes scatter by skipping already-full rows.
|