开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第36天,点击查看活动详情
✨欢迎关注🖱点赞🎀收藏⭐留言✒
🔮本文由京与旧铺原创,csdn首发!
😘系列专栏:算法学习
💻首发时间:🎞2022年12月31日🎠
🀄如果觉得博主的文章还不错的话,请三连支持一下博主哦
🎧作者是一个新人,在很多方面还做的不好,欢迎大佬指正,一起学习哦,冲冲冲
⭐️855. 考场就座⭐️
🔐题目详情
难度中等
在考场里,一排有 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上。
💡解题思路
解题思路:
本题需要我们设计一个类,能够将考生安排在一个离其他考生尽量远的位置,如果存在多种方案,选择编号最小的一个位置。
最简单的思路就是使用一个有序集合来储存已经选择的位置编号,然后在选择新来考生位置时,遍历所有的编号,找到所有的区间,并计算所有区间中,离与的最远距离的较小编号的点。
//有序集合TreeSet,按照从小到大的顺序储存考生的座位编号
private TreeSet<Integer> seats;
private int n;
public ExamRoom(int n) {
this.n = n;
seats = new TreeSet<>();
}
在相邻两个位置间,尽量离两位置远的长度为,编号较小位置编号为。
当然,可能存在取左右端点能够离其他考生更远,所以还得考虑取左右端点时的离其他考生的距离,如果该长度更大,或者在相同最远长度情况下,考虑左右端点的情况。
考生离开的话就直接将该考生所做座位编号从有序集合中移除即可。
这样做的话每一次都需要遍历有序集合中的元素,找到距离其他考生尽量远的点,时间复杂度为,我们可以使用优先级队列储存区间来优化查找的过程,这样我们在找中间区间找离其他考生尽量远的点时,可以直接通过优先级队列找到离其他考生尽量远的点,并且可以找到编号尽量小的点。
就是将区间直接放在优先队列中,通过优先队列每次弹出距离最大的一个区间,再选择区间的中点即可。
我们每次从优先队列中弹出一个区间,假设这是所有区间中距离最大的一个区间,我们将它与选择点(左端点)和选择点(右端点)的距离进行比较,若更大,那么我们就选择中点为,会产生新的区间和,放入优先队列中;若选择最左最右点,那么也会产生新的区间或。
当位置p考生离开时,如果不是最左或者最右的考生离开,则在有序集合中,还需要将比p稍微小的数seats.lower(p),比p稍微大的数seats.higher(p),之间的区间放入优先级队列当中,并把p从有序集合中移除。
当然,这样处理离开的考生的位置编号,在优先级队列中就会产生无效区间,所以在取出区间时,需要进行判断,如果判断是无效区间,需要重新取一个区间进行后续计算。
无效区间判断规则如下,假设取出的区间为:
- 若不在有序集合中,则该区间为无效区间。
- 若,则该区间为无效区间。
🔑源代码
有序集合实现代码:
class ExamRoom {
//有序集合TreeSet,按照从小到大的顺序储存考生的座位编号
private TreeSet<Integer> seats;
private int n;
public ExamRoom(int n) {
this.n = n;
seats = new TreeSet<>();
}
public int seat() {
//考场为空的时候,做座位0,并返回0
if (seats.size() == 0) {
seats.add(0);
return 0;
}
//默认取左端点
int left = seats.first();
int len = left;
int obSeat = 0;
//遍历R
for (int right : seats) {
if ((right - left) / 2 > len) {
len = (right - left) / 2;
obSeat = (left + right) / 2;
}
//更新L
left = right;
}
//检测右端点是否可行
int d = n - 1 - seats.last();
if (d > len) {
len = d;
obSeat = n - 1;
}
seats.add(obSeat);
return obSeat;
}
public void leave(int p) {
seats.remove(p);
}
}
有序集合+优先级队列优化代码:
class ExamRoom {
//有序集合
private TreeSet<Integer> seats;
private int n;
//优先级队列记录区间,以区间长度最长的为堆顶
private PriorityQueue<int[]> pq;
public ExamRoom(int n) {
this.n = n;
seats = new TreeSet<>();
pq = new PriorityQueue<>((a, b) -> {
int d1 = (a[1] - a[0]) / 2;
int d2 = (b[1] - b[0]) / 2;
if (d1 != d2) return d2 - d1;
else return a[0] - b[0];
});
}
public int seat() {
if (seats.size() == 0) {
seats.add(0);
return 0;
}
//最左边界
int d1 = seats.first();
//最右边界
int d2 = n - 1 - seats.last();
while (seats.size() >= 2) {
int[] t = pq.poll();
//排除无效区间
if (!seats.contains(t[0]) || !seats.contains(t[1]) || seats.higher(t[0]) != t[1]) continue;
int d3 = (t[1] - t[0]) / 2;
if (d3 < d2 || d3 <= d1) {
//选择边界
pq.offer(new int[]{t[0], t[1]});
break;
}
int mid = (t[0] + t[1]) / 2;
pq.offer(new int[]{t[0], mid});
pq.offer(new int[]{mid, t[1]});
seats.add(mid);
return mid;
}
//选择两边界
if (d2 > d1) {
pq.offer(new int[]{seats.last(), n - 1});
seats.add(n - 1);
return n - 1;
}
pq.offer(new int[]{0, seats.first()});
seats.add(0);
return 0;
}
public void leave(int p) {
//如果不是最左或者最右边的考生,需要添加区间[比p小的数中最大的数, 比p大的数中最小的数]
if (seats.first() != p && seats.last() != p) {
pq.offer(new int[]{seats.lower(p), seats.higher(p)});
}
seats.remove(p);
}
}
🌱总结
本题为有序集合+优先级队列数据结构设计题。