在考场里,一排有 N 个座位,分别编号为 0, 1, 2, ..., N-1 。
当学生进入考场后,他必须坐在能够使他与离他最近的人之间的距离达到最大化的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)
返回 ExamRoom(int N) 类,它有两个公开的函数:其中,函数 ExamRoom.seat() 会返回一个 int (整型数据),代表学生坐的位置;函数 ExamRoom.leave(int p) 代表坐在座位 p 上的学生现在离开了考场。每次调用 ExamRoom.leave(p) 时都保证有学生坐在座位 p 上。
示例:
输入:["ExamRoom","seat","seat","seat","seat","leave","seat"], [[10],[],[],[],[],[4],[]]
输出:[null,0,9,4,2,null,5]
解释:
ExamRoom(10) -> null
seat() -> 0,没有人在考场里,那么学生坐在 0 号座位上。
seat() -> 9,学生最后坐在 9 号座位上。
seat() -> 4,学生最后坐在 4 号座位上。
seat() -> 2,学生最后坐在 2 号座位上。
leave(4) -> null
seat() -> 5,学生最后坐在 5 号座位上。
提示:
1 <= N <= 10^9
在所有的测试样例中 ExamRoom.seat() 和 ExamRoom.leave() 最多被调用 10^4 次。
保证在调用 ExamRoom.leave(p) 时有学生正坐在座位 p 上。
解法一:有序集合
首先,对于给定的区间
[
L
,
R
]
[L, R]
[L,R],即
L
,
R
L,R
L,R的座位已经选择,当我们要在这个区间进行放置的时候,为了使离他最近的人之间的距离达到最大化,我们应该将座位选择在中点即
(
L
+
R
)
/
2
(L+R)/2
(L+R)/2,所能够得到的距离定义为
(
R
−
L
)
/
2
(R-L)/2
(R−L)/2。
例如:
[1, 4]
我们应该选择在2点,距离最近的人距离为1, 即1 2 . 4
[1, 5]
我们应该选择在3点,距离最近的人距离为2,即1.3.5
Seat():
接下来,我们创建一个有序集合来保存已经选了的座位,最开始当集合为空时,必然选择下标为0的座位。若集合不为空,则遍历集合中所有的区间,选择出一个距离最大的中点出来,最后再特判一下选择0点和n-1点的情况。
对于0点和n-1点来说:
0点的距离为:集合中第一个元素-0, 即set.first()
n-1点的距离为:(n-1) - 集合中最后一个元素,即n-1-set.last()
Leave():
对于删除操作,我们直接在有序集合中将要删除的座位移除即可。
- 时间复杂度:seat(): O ( n ) O(n) O(n), leave(): O ( l o g n ) O(logn) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
class ExamRoom {
TreeSet<Integer> set;
int n;
public ExamRoom(int n) {
this.n = n;
set = new TreeSet<>();
}
public int seat() {
if (set.size() == 0) {set.add(0); return 0;} //没有人时,一定返回0
int pre = set.first(), ans = set.first(), idx = 0; //初始话为选择最左的长度
for (int x : set) {
if (ans < (x - pre) / 2) {
ans = (x - pre) / 2;
idx = (x + pre) / 2;
}
pre = x;
}
//最右进行判断
int d = n - 1 - set.last();
if (ans < d) {ans = d; idx = n - 1;}
set.add(idx);
return idx;
}
public void leave(int p) {
set.remove(p);
}
}
解法二:有序集合 + 优先队列
Seat():
在解法一的基础上,我们可以通过优先队列来优化我们区间选择的操作,在解法一我们通过遍历来求的拥有最大距离的区间,再特判一下最左最右端点。我们可以将区间直接放在优先队列中,通过优先队列每次弹出距离最大的一个区间,再选择区间的中点即可。
我们每次从优先队列中弹出一个区间
[
L
,
R
]
[L,R]
[L,R],这是所有区间中距离最大的一个区间,我们将它与选择
0
0
0点和选择
n
−
1
n-1
n−1点的距离进行比较,若更大,那么我们就选择中点为
m
i
d
=
(
L
+
R
)
/
2
mid=(L+R)/2
mid=(L+R)/2,会产生新的区间
[
L
,
m
i
d
]
[L,mid]
[L,mid]和
[
m
i
d
,
R
]
[mid, R]
[mid,R],放入优先队列中;若选择最左最右点,那么也会产生新的区间
[
0
,
s
e
t
.
f
i
r
s
t
(
)
]
[0,set.first()]
[0,set.first()]或
[
s
e
t
.
l
a
s
t
(
)
,
n
−
1
]
[set.last(), n - 1]
[set.last(),n−1]。
Leave():
对于删除操作,当我们删除集合中第一个元素和最后一个元素时,并不会产生新的区间。当删除集合中间的元素时,如
[
L
,
m
i
d
,
R
]
[L,mid,R]
[L,mid,R],我们删除mid点会产生新的区间
[
L
,
R
]
[L, R]
[L,R]将新区间添加进我们的优先队列中即可。
而我们还需要在队列中将以前的区间
[
L
,
m
i
d
]
和
[
m
i
d
,
R
]
[L,mid]和[mid, R]
[L,mid]和[mid,R]进行删除,我们使用延迟删除的技巧,因此在
s
e
a
t
(
)
seat()
seat()中我们从队列中弹出的区间可能是需要删除的,我们判断区间的两个端点是否在集合set中,若不在代表需要删除,并且区间中间不能有任何座位被选取。
- 时间复杂度:seat(): O ( l o g n ) O(logn) O(logn), leave(): O ( l o g n ) O(logn) O(logn)
- 空间复杂度: O ( n ) O(n) O(n)
class ExamRoom {
PriorityQueue<int[]> q;
TreeSet<Integer> set;
int n;
public ExamRoom(int n) {
this.n = n;
q = new PriorityQueue<>((a, b) -> {
int d1 = (a[1] - a[0]) / 2, d2 = (b[1] - b[0]) / 2;
return d1 == d2 ? a[0] - b[0] : d2 - d1; //当长度相等时,坐标更小先弹出,当不相等时,长度更大的先弹出
});
set = new TreeSet<>(); //创建有序集合
}
public int seat() {
if (set.size() == 0) { set.add(0); return 0;} //1.没有人时,一定返回0
int d1 = set.first(), d2 = n - 1 - set.last(); //获取最左和最右放置学生能获取的长度
while (set.size() >= 2) { //2.大于等于两个人的时候,可以选择最左最右 或者中间的区间
int[] t = q.poll();
if (!set.contains(t[0]) || !set.contains(t[1]) || set.higher(t[0]) != t[1]) continue; //无效区间,某个端点已经被删除
int d3 = (t[1] - t[0]) / 2;
if (d3 <= d1 || d3 < d2) {q.add(new int[]{t[0], t[1]}); break;}; //选择最左或者最右
int mid = (t[0] + t[1]) / 2; //选择终点
q.add(new int[]{t[0], mid});
q.add(new int[]{mid, t[1]});
set.add(mid);
return mid;
}
//3.选择最左或者最右的位置
int l = 0, r = set.first(), sel = 0;
if (d1 < d2) {l = set.last(); r = n - 1; sel = n - 1;}
q.add(new int[]{l, r});
set.add(sel);
return sel;
}
public void leave(int p) {
if (p != set.first() && p != set.last()) q.add(new int[]{set.lower(p), set.higher(p)}); //如果不是删除两端点, 那么会增加新区间
set.remove(p);
}
}