
题解:会议室 3
使用扫描线算法。这个题用线段树来做其实挺直观的,复杂度也能做到 nlognnlogn 但是实在是代码量太大了。所以就看看这个题有没有一些没用到的线段树的功能。进而发现这个题目,只需要支持查询,不需要支持修改。因此就大概率存在不用线段树的方法。进一步的看,这个题并不是每次 ask 都要求你马上返回一个结果,而是一口气给你一堆 ask,然后一口气 return 一堆结果。因此想到可以用离线算法的思想。因此我们可以把 ask 里面的 Intervals 也和正常开会的 Intervals 混在一起去排序和处理。排序的方法是,把 Interval 拆开成为起点和终点,位置优先排序,位置相同的终点在前,起点在后。排序之后从左到右处理,也就是扫描线算法了。在扫描的过程中,维护一个 ask_set 代表此时扫描线扫到的询问区间有哪些,那么如果此时 meetings 的个数 >= 了房间个数 rooms,这些 ask_set 里面的区间,就都需要标记为 False(无法安排会议)。
一些细节要处理的是,由于区间的跨度可能很大,有连个地方需要去处理标记情况,一个是真实的会议开始的时间点,一个是当一个 ask 开始的时间点。
1.扫描线:start是1,end是-1,填写端点val
2.排序:先安装val排序,如果val相等则end位-1的排序在前面
**3.**如果当前点是ask,
判断下当前节点是否是开始:
累加sum>=rooms,表示当前不能添加该会议了
<rooms,则将当前的ask添加到ask_set,因为后面可能有房间导致不满足调整
不是ask
对sum += n.flag进行累加
如果sum >= rooms,表示前面ask_set里面的ask都需要标记为false,且清空ask_set(后面可能还不满足,也可能会重复赋值为false)
cpp
class Solution {
public:
/**
* @param intervals: the intervals
* @param rooms: the sum of rooms
* @param ask: the ask
* @return: true or false of each meeting
*/
struct Node {
int val;
int flag;
bool ask;
int index;
Node(int v, int f, bool a, int i) {
val = v;
flag = f;
ask = a;
index = i;
}
};
vector<bool> meetingRoomIII(vector<vector<int>>& intervals, int rooms,
vector<vector<int>>& ask) {
// Write your code here.
int len = intervals.size();
if (len <= 0) {
return {};
}
vector<Node> nodes;
for (auto& v : intervals) {
Node node1(v[0], 1, false, -1);
Node node2(v[1], -1, false, -1);
nodes.push_back(node1);
nodes.push_back(node2);
}
for (int i = 0; i < ask.size(); ++i) {
vector<int>& v = ask[i];
Node node1(v[0], 1, true, i);
Node node2(v[1], -1, true, i);
nodes.push_back(node1);
nodes.push_back(node2);
}
sort(nodes.begin(), nodes.end(), [](Node& a, Node& b) {
if (a.val < b.val) {
return true;
} else if (a.val == b.val && a.flag < b.flag) {
return true;
}
return false;
});
unordered_set<int> ask_set;
vector<bool> result(ask.size(), true);
int sum = 0;
for (auto& n : nodes) {
if (n.ask) {
if (n.flag > 0) {
if (sum >= rooms) {
result[n.index] = false;
} else {
ask_set.insert(n.index);
}
} else {
ask_set.erase(n.index);
}
} else {
sum += n.flag;
if (sum >= rooms) {
for (auto index : ask_set) {
result[index] = false;
}
ask_set.clear();
}
}
}
return result;
}
};
ask_set.erase(n.index) 确实会删除 ,但它只删除询问结束时 的那个询问。问题在于:在询问结束之前,如果期间发生了多次"满员",这个询问会被反复遍历到。
具体场景
假设有一个询问 A,它的区间是 [1, 100]。在这段时间内,已有会议导致房间多次满员(比如在时刻 10、30、50、70、90 各满员一次)。
扫描过程:
| 时刻 | 事件 | ask_set 内容 | 动作 |
|---|---|---|---|
| 1 | A 开始 | {A} |
加入 A |
| 10 | 满员 | {A} |
遍历 {A},标记 A 为 false ❌ |
| 30 | 满员 | {A} |
再次 遍历 {A},重复标记 ❌ |
| 50 | 满员 | {A} |
再次 遍历 {A} ❌ |
| 70 | 满员 | {A} |
再次 遍历 {A} ❌ |
| 90 | 满员 | {A} |
再次 遍历 {A} ❌ |
| 100 | A 结束 | {} |
删除 A |
虽然 A 在第 10 时刻就已经被标记为 false,但它直到第 100 时刻才被 erase。中间的每一次满员,都会重新遍历一遍 A。
如果有 M 个这样的询问,每个都跨越多轮满员,时间复杂度就是 O(K × M),K 是满员次数,M 是询问数,这就超时了。
加上 clear() 后
| 时刻 | 事件 | ask_set 内容 | 动作 |
|---|---|---|---|
| 1 | A 开始 | {A} |
加入 A |
| 10 | 满员 | {A} |
遍历 {A},标记 false,clear() ✅ |
| 30 | 满员 | {} |
空,不遍历 |
| 50 | 满员 | {} |
空,不遍历 |
| ... | ... | {} |
不再重复遍历 ✅ |
| 100 | A 结束 | {} |
erase 无影响 |
每个询问最多被遍历一次,总复杂度降到 O(N + M)。
核心区别
| 操作 | erase 单独 |
erase + clear |
|---|---|---|
| 何时移除询问 | 询问结束时 | 询问第一次遇到满员时(或正常结束) |
| 同一询问被遍历次数 | 多次(每次满员都遍历) | 最多 1 次 |
| 最坏复杂度 | O(K × M) | O(N + M) |
一句话总结
erase 只在询问自然结束 时才移除它,无法阻止它在存活期间被反复遍历。
clear() 则在第一次满员时就把所有等待中的询问一次性清空,保证每个询问只被处理一次。两者配合,才能避免超时。