1847. Closest Room
Problem
There is a hotel with n rooms. The rooms are represented by a 2D integer array rooms where rooms[i] = [roomIdᵢ, sizeᵢ] denotes that there is a room with room number roomIdᵢ and size equal to sizeᵢ. Each roomIdᵢ is guaranteed to be unique.
You are also given k queries in a 2D array queries where queries[j] = [preferredⱼ, minSizeⱼ]. The answer to the jᵗʰ query is the room number id of a room such that:
- The room has a size of at least
minSizeⱼ, and abs(id - preferredⱼ)is minimized, whereabs(x)is the absolute value ofx.
If there is a tie in the absolute difference, then use the room with the smallest such id. If there is no such room, the answer is -1.
Return an array answer of length k where answer[j] contains the answer to the jᵗʰ query.
https://leetcode.cn/problems/closest-room/
Example 1:
Input:
rooms = [[2,2],[1,2],[3,2]], queries = [[3,1],[3,3],[5,2]]
Output:[3,-1,3]
Explanation: The answers to the queries are as follows:
Query =[3,1]: Room number 3 is the closest asabs(3 - 3) = 0, and its size of 2 is at least 1. The answer is 3.
Query =[3,3]: There are no rooms with a size of at least 3, so the answer is -1.
Query =[5,2]: Room number 3 is the closest asabs(3 - 5) = 2, and its size of 2 is at least 2. The answer is 3.
Example 2:
Input:
rooms = [[1,4],[2,3],[3,5],[4,1],[5,2]], queries = [[2,3],[2,4],[2,5]]
Output:[2,1,3]
Explanation: The answers to the queries are as follows:
Query =[2,3]: Room number 2 is the closest asabs(2 - 2) = 0, and its size of 3 is at least 3. The answer is 2.
Query =[2,4]: Room numbers 1 and 3 both have sizes of at least 4. The answer is 1 since it is smaller.
Query =[2,5]: Room number 3 is the only room with a size of at least 5. The answer is 3.
Constraints:
n == rooms.length1 <= n <= 10⁵k == queries.length1 <= k <= 10⁴1 <= roomIdᵢ, preferredⱼ <= 10⁷1 <= sizeᵢ, minSizeⱼ <= 10⁷
Test Cases
class Solution:
def closestRoom(self, rooms: List[List[int]], queries: List[List[int]]) -> List[int]:import pytest
from solution import Solution
from solution_fast import Solution as SolutionFast
from solution3 import Solution as Solution3
@pytest.mark.parametrize('rooms, queries, expected', [
([[2,2],[1,2],[3,2]], [[3,1],[3,3],[5,2]], [3,-1,3]),
([[1,4],[2,3],[3,5],[4,1],[5,2]], [[2,3],[2,4],[2,5]], [2,1,3]),
(
[[23,22],[6,20],[15,6],[22,19],[2,10],[21,4],[10,18],[16,1],[12,7],[5,22]],
[[12,5],[15,15],[21,6],[15,1],[23,4],[15,11],[1,24],[3,19],[25,8],[18,6]],
[12,10,22,15,23,10,-1,5,23,15]
),
])
class Test:
def test_solution(self, rooms, queries, expected):
sol = Solution()
assert sol.closestRoom(rooms.copy(), queries.copy()) == expected
def test_solution_fast(self, rooms, queries, expected):
sol = SolutionFast()
assert sol.closestRoom(rooms.copy(), queries.copy()) == expected
def test_solution3(self, rooms, queries, expected):
sol = Solution3()
assert sol.closestRoom(rooms.copy(), queries.copy()) == expected
Thoughts
对于一个 query,可以把所有 size >= minSize 的房间找出来,在这些房间中找距离 preferred 最近的编号。如果这些房间是按照编号排序的,就可以用二分查找。
如果下一个 query 的 minSize 更小一些,那么可以往这个房间集合中再加入那些 size 不小于新 query 的 minSize 的房间,如果还能保持按编号排序,就还可以用二分查找。
所以对 queries 按 minSize 降序排列,这样每下一个 query 的 minSize 都不会比前一个大。对 rooms 也按 size 降序排列,用两个下标分别跟踪 queries 和 rooms,可以方便地对每个新 query 找出增量的房间。
关键在于用一个「能保持有序」的容器保存按 minSize 筛出来的房间,在不断加入新房间的同时保持按房间编号排序。
暂不考虑 grantjenks/python-sortedcontainers: Python Sorted Container Types: Sorted List, Sorted Dict, and Sorted Set 之类的第三方工具,很容易想到的是二叉查找树(Binary Search Tree),或其变体如 AVL 树(Georgy Adelson-Velsky and Evgenii Landis Tree)、红黑树(Red Black Tree)等。
先试了一下 BST,果然很慢(因为非常不平衡)。只好修改插入的方法,使其成为 AVL 树,快了十倍。本题不涉及删除节点,不用红黑树也可以。
AVL 树的详细信息参见 DSA AVL Trees。
稍微修改一下树的查找方法,如果目标数字找不到,就返回小于目标数的最大值、以及大于目标树的最小值,这对于 BST 或其变体都很简单。
时间复杂度是 O(n log n + k log k + k log n),其中 O(n log n) 是排序 rooms 和不断构建可选房间的有序集合的时间;O(k log k) 是排序 queries 的时间;O(k log n) 是对所有 queries,借助 BST 查找等于或最接近 query 中 preferred 房间编号的时间。
空间复杂度是 O(n + k),其中 O(n) 是可选房间有序集合的空间(rooms 可以 in-place 排序);O(k) 是对 queries 排序但需要保留原始下标所需的空间。
提交后的速度不是很快,也就 7+%。可能是 AVL 树的常数系数太大了吧。实际上对于 Python 而言,直接用数组(list)配合标准库里的 bisect — Array bisection algorithm 就可以做到速度非常快(sortedcontainers 底层也是利用 list 和 bisect 库来实现的)。在本题限定的量级内,直接用 list 就已经足够快了(100%)(虽然插入的复杂度是 O(n))。
Code
AVL Tree
Runtime beats
7+%:
class TreeNode:
def __init__(self, val: int, left: 'TreeNode' = None, right: 'TreeNode' = None):
self.val = val
self.left = left
self.right = right
self.height = 1
@property
def balance(self) -> int:
l = self.left.height if self.left else 0
r = self.right.height if self.right else 0
return l - r
@property
def left_balance(self) -> int:
return self.left.balance if self.left else 0
@property
def right_balance(self) -> int:
return self.right.balance if self.right else 0
def re_height(self) -> None:
l = self.left.height if self.left else 0
r = self.right.height if self.right else 0
self.height = 1 + max(l, r)
def rotate_left(self) -> 'TreeNode':
right = self.right
self.right, right.left = right.left, self
self.re_height()
right.re_height()
return right
def rotate_right(self) -> 'TreeNode':
left = self.left
self.left, left.right = left.right, self
self.re_height()
left.re_height()
return left
class AVLTree:
def __init__(self) -> None:
self._root: TreeNode = None
def insert(self, val: int) -> None:
if self._root is None:
self._root = TreeNode(val)
return
ancestors = []
node = self._root
while True:
ancestors.append(node)
if val < node.val:
if node.left is None:
node.left = child = TreeNode(val)
break
else:
node = node.left
else:
if node.right is None:
node.right = child = TreeNode(val)
break
else:
node = node.right
while ancestors:
node = ancestors.pop()
if child.val < node.val:
node.left = child
else:
node.right = child
node.re_height()
balance = node.balance # left height - right height
if balance > 1:
if node.left and node.left.balance < 0: # Left-Right
node.left = node.left.rotate_left()
node = node.rotate_right() # Left-Left & Left-Right
elif balance < -1:
if node.right and node.right.balance > 0: # Right-Left
node.right = node.right.rotate_right()
node = node.rotate_left() # # Right-Right & Right-Left
child = node
self._root = child
def find(self, val: int) -> tuple[int, int, int]:
"""Finds `val`. If not found, return the largest value < val, and the smallest value > val."""
lt = gt = None
node = self._root
while node:
if node.val == val:
return val, None, None
elif node.val > val:
gt = node.val
node = node.left
else:
lt = node.val
node = node.right
return None, lt, gt
class Solution:
def closestRoom(self, rooms: list[list[int]], queries: list[list[int]]) -> list[int]:
k = len(queries)
queries = [(q[0], q[1], j) for j, q in enumerate(queries)]
queries.sort(key=lambda q: q[1], reverse=True)
answer = [-1] * k
n = len(rooms)
rooms.sort(key=lambda r: r[1], reverse=True)
valid_rooms = AVLTree()
i = 0
for preferred, min_size, j in queries:
while i < n and rooms[i][1] >= min_size:
valid_rooms.insert(rooms[i][0])
i += 1
ans, lt, gt = valid_rooms.find(preferred)
if ans is None:
ans = lt
if gt is not None and (ans is None or gt - preferred < preferred - ans):
ans = gt
if ans is not None: answer[j] = ans
return answer
Python list - Fast
Runtime beats
100%:
from bisect import bisect_left, insort
class Solution:
def closestRoom(self, rooms: list[list[int]], queries: list[list[int]]) -> list[int]:
k = len(queries)
queries = [(q[0], q[1], j) for j, q in enumerate(queries)]
queries.sort(key=lambda q: q[1], reverse=True)
answer = [-1] * k
n = len(rooms)
rooms.sort(key=lambda r: r[1], reverse=True)
valid_rooms = []
i = 0
for preferred, min_size, j in queries:
while i < n and rooms[i][1] >= min_size:
insort(valid_rooms, rooms[i][0])
i += 1
if valid_rooms:
idx = bisect_left(valid_rooms, preferred)
if idx == len(valid_rooms) or idx > 0 and preferred - valid_rooms[idx-1] <= valid_rooms[idx] - preferred:
idx -= 1
answer[j] = valid_rooms[idx]
return answer
Monotonic Stack
在 2940. Find Building Where Alice and Bob Can Meet 中尝试 基于单调栈 + 二分搜索的解法 时,觉得这道题也可以用类似的逻辑处理。
核心区别只有两个。一个是本题需要往两个方向找,既要找 preferred 右边第一个,也要找 preferred 左边第一个,可以通过两次循环达成。另一个是 preferred 不一定是存在的房间号,不能用跟 problem 2940 一样的方式(即用当前扫描到的房间号查出 prefer 此房间号的所有查询),需要对 preferred 和 roomId 做分段匹配。
各种边界条件细节需要小心处理。
时间复杂度是 O(n log n + k log k + k log n),空间复杂度 O(n + k),跟上边一样。实际运行时间跟 problem 2940 也很像,用单调栈比用有序集合慢一倍。
from bisect import bisect_right
gt = lambda a, b: a > b
lt = lambda a, b: a < b
class Solution:
def closestRoom(self, rooms: list[list[int]], queries: list[list[int]]) -> list[int]:
rooms.sort() # Sort rooms by roomId.
queries = [(q[0], q[1], j) for j, q in enumerate(queries)] # Remember origin query index before sort.
queries.sort() # Sort queries by preferred.
answer = [-1] * len(queries)
for done, valid in enumerate((gt, lt)):
j = len(queries) - 1
while j >= 0 and valid(queries[j][0], rooms[-1][0]): # Skip no-result queries for current direction.
j -= 1
stack = []
for i in range(len(rooms) - 1, -1, -1):
size = rooms[i][1]
while stack and rooms[stack[-1]][1] <= size:
stack.pop()
stack.append(i)
while j >= 0 and (i == 0 or valid(queries[j][0], rooms[i-1][0])):
preferred, min_size, qid = queries[j]
j -= 1
if answer[qid] == preferred: continue
idx = bisect_right(stack, -min_size, key=lambda x: -rooms[x][1]) - 1
if idx < 0: continue
if answer[qid] == -1 or preferred - rooms[stack[idx]][0] <= answer[qid] - preferred:
answer[qid] = rooms[stack[idx]][0]
if done: break
rooms.reverse()
queries.reverse()
return answer
评论需要 JavaScript。