[Med] LeetCode 855. Exam Room
链接: https://leetcode.com/problems/exam-room/
题目描述:
In an exam room, there are N seats in a single row, numbered 0, 1, 2, …, N-1.
When a student enters the room, they must sit in the seat that maximizes the distance to the closest person. If there are multiple such seats, they sit in the seat with the lowest number. (Also, if no one is in the room, then the student sits at seat number 0.)
Return a class ExamRoom(int N) that exposes two functions: ExamRoom.seat() returning an int representing what seat the student sat in, and ExamRoom.leave(int p) representing that the student in seat number p now leaves the room. It is guaranteed that any calls to ExamRoom.leave§ have a student sitting in seat p.
在考场里,一排有 N 个座位,分别编号为 0, 1, 2, …, N-1 。
当学生进入考场后,他必须坐在能够使他与离他最近的人之间的距离达到最大化的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)
返回 ExamRoom(int N) 类,它有两个公开的函数:其中,函数 ExamRoom.seat() 会返回一个 int (整型数据),代表学生坐的位置;函数 ExamRoom.leave(int p) 代表坐在座位 p 上的学生现在离开了考场。每次调用 ExamRoom.leave§ 时都保证有学生坐在座位 p 上。
Example 1:
Input: [“ExamRoom”,“seat”,“seat”,“seat”,“seat”,“leave”,“seat”], [[10],[],[],[],[],[4],[]]
Output: [null,0,9,4,2,null,5]
Explanation:
ExamRoom(10) -> null
seat() -> 0, no one is in the room, then the student sits at seat number 0.
seat() -> 9, the student sits at the last seat number 9.
seat() -> 4, the student sits at the last seat number 4.
seat() -> 2, the student sits at the last seat number 2.
leave(4) -> null
seat() -> 5, the student sits at the last seat number 5.
Note:
- 1 <= N <= 10^9
- ExamRoom.seat() and ExamRoom.leave() will be called at most 10^4 times across all test cases.
- Calls to ExamRoom.leave( p ) are guaranteed to have a student currently sitting in seat number p.
Tag: TreeSet / PriorityQueue
解题思路
这道题是 849. Maximize Distance to Closest Person的一个升级版本。在这里我们要动态的分配最大的空位给每一个人。首先我们看看这个数据规模,是 10^4次call。所以每一次的查找时间应该冗余比较大。一开始想到的比较naive的算法无非就是每一次O(n)时间遍历数组,像849一样操作插入。然后再O(n)的时间操作删除。这样肯定就TLE了,所以我们要找到一种更快地办法。
解法一:
因为每一次我们都要找到一个最大的空挡,所以我们需要考虑快速的获取这个最大空挡的位置。我在这里使用了priorityqueue(), 我们按照空挡的范围从大到小进行排序,注意,这里如果两个空挡的宽度一样,我们就获取空档起始坐标较小的那一个空挡。这道题目还有一个比较麻烦的点,因为在seat()方法当中我们的操作都是先获取一个空挡,然后将新的人放在这个空挡的中间,然后原来的空挡会被分为两个不同的空挡。将新的两个空挡再放回priorityqueue当中。但是对于两端的人来,只有最多一面是有人的。那我们怎么办?我的做法是首先放进去一个范围在[-1, N]的空挡。当我们从pq当中获取最大区间的时候,如果这个区间空挡左边为-1,说明当前区间当中包含了左边的这堵墙。我们的分割方法当然是将元素放在最左边而不是中间,所以分割方法是创建两个新的区间,一个是[-1,0],一个是[0, end]。同理,如果获取的区间包含末尾的话,那么这个区间的范围也会被分割为[start, N-1]和[N-1, N]。这样我们把corner case考虑进去之后,其余的每一次都把区间一分为二就可以了。
因为leave的时候我们遍历了整个pq, 所以时间复杂度为O(N),插入的时间复杂度是O(logn)
class ExamRoom {
PriorityQueue<Interval> pq;
int N;
public ExamRoom(int N) {
this.pq = new PriorityQueue<>((a, b) -> a.dist != b.dist? b.dist - a.dist : a.x - b.x);
this.N = N;
pq.add(new Interval(-1, N));
}
// O(logn): poll top candidate, split into two new intervals
public int seat() {
int seat = 0;
Interval interval = pq.poll();
if (interval.x == -1) seat = 0;
else if (interval.y == N) seat = N - 1;
else seat = (interval.x + interval.y) / 2;
pq.offer(new Interval(interval.x, seat));
pq.offer(new Interval(seat, interval.y));
return seat;
}
// O(n)Find head and tail based on p. Delete and merge two ends
public void leave(int p) {
Interval head = null, tail = null;
for (Interval interval : pq) {
if (interval.x == p) tail = interval;
if (interval.y == p) head = interval;
if (head != null && tail != null) break;
}
// Delete
pq.remove(head);
pq.remove(tail);
// Merge
pq.offer(new Interval(head.x, tail.y));
}
class Interval {
int x, y, dist;
public Interval(int x, int y) {
this.x = x;
this.y = y;
if (x == -1) {
this.dist = y;
} else if (y == N) {
this.dist = N - 1 - x;
} else {
this.dist = Math.abs(x - y) / 2;
}
}
}
}
解法二:
这个办法将leave()方法的时间复杂度下降成了O(logn)。之前我们使用pq的情况下,我们必须遍历整个pq才能知道这个离开位置分割了哪两个区间的范围,知道这两个区间我们才能合并这两个区间。但是在这里我们使用了TreeSet把每一个有人的座位都记录下来,使用treeset.lower(pos)和treeset.upper(pos)两个方法获取左右两个区间的起始位置。然后在另一个储存区间的treeset当中,利用重写的equal()方法获取这两个区间的具体信息。然后合并这两个区间。这样做的话时间复杂度就降低为了O(logn)
class ExamRoom {
TreeSet<interval> distance;
TreeSet<Integer> position;
int N;
public ExamRoom(int N) {
//int[0] = 区间左边的坐标,int[1] = 区间右边的坐标
distance = new TreeSet<>((a, b) -> a.space != b.space? b.space - a.space : a.first - b.first);
position = new TreeSet<>();
distance.add(new interval(-1, N));
this.N = N;
}
public int seat() {
int seat = 0;
interval maxDist = distance.pollFirst();
if(maxDist.first == -1){
seat = 0;
distance.add(new interval(seat, maxDist.last));
}else if(maxDist.last == N){
seat = N-1;
distance.add(new interval(maxDist.first, seat));
}else{
seat = (maxDist.last + maxDist.first)/2;
distance.add(new interval(maxDist.first, seat));
distance.add(new interval(seat, maxDist.last));
}
position.add(seat);
return seat;
}
public void leave(int p) {
int startIdx = position.lower(p) == null? -1 : position.lower(p);
int endIdx = position.higher(p) == null? N : position.higher(p);
distance.remove(new interval(startIdx, p));
distance.remove(new interval(p, endIdx));
distance.add(new interval(startIdx, endIdx));
position.remove(p);
}
class interval{
int first, last, space;
String id; // for O(1) lookup
public interval(int first, int last){
this.first = first;
this.last = last;
if(first == -1)
space = last;
else if(last == N)
space = N-1-first;
else
space = Math.abs(last-first)/2;
this.id = first + "_" + last;
}
// this is for TreeSet remove (Object o) method.为了快速获取treeset当中的这个区间
public boolean equals(interval i) {
return id.equals(i.id);
}
}
}