class Solution {
public:
int mostBooked(int n, vector<vector<int>>& meetings) {
ranges::sort(meetings, {}, [](auto& m) {return m[0];});
// 空闲会议室
priority_queue<int, vector<int>, greater<>> idle;
for (int i = 0; i < n; i++)
{
idle.push(i);
}
// (结束事件, 会议编号)
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> using_;
// 每个会议室使用次数
vector<int> cnt(n);
for (auto& m : meetings)
{
long long start = m[0], end = m[1];
// 在start时刻有的空闲会议室
while (!using_.empty() && using_.top().first <= start)
{
idle.push(using_.top().second);
using_.pop();
}
int i;
// 如果有空闲会议室
if (!idle.empty())
{
i = idle.top();
idle.pop();
}
else // 没有空闲会议室
{
auto [e, room] = using_.top();
i = room;
using_.pop();
end += e - start;
}
// 使用一个会议室
using_.emplace(end, i);
cnt[i]++;
}
return ranges::max_element(cnt) - cnt.begin();
}
};
1. 核心目标与定位
该算法旨在解决一个受限资源分配问题:
- 输入:n 个会议室,一组带有开始和结束时间的会议。
- 规则:按开始时间顺序处理;空闲时优先用小编号会议室;全满时等待最早空闲的会议室并整体推迟会议。
- 输出:使用频率最高的会议室编号(若次数相同取编号最小者)。
2. 技术选型与数据结构(为什么这么选?)
报告中通过对比,明确了三个关键工具:
- 排序(Sort):通过
ranges::sort确保处理逻辑符合时间线(O(M \log M))。 - 双小顶堆(Dual Min-Heaps):
idle:存储空闲会议室编号,保证 O(log N) 获取“编号最小”的资源。using_:存储{结束时间, 编号},保证 O(log N) 获取“最早结束”的会议室。
- 计数数组(Vector):O(1) 随机访问,记录每个房间的使用频次。
3. 算法执行流程(核心逻辑)
算法通过一个主循环对会议进行“模拟分发”,分为三个关键动作:
- 释放(Release):在处理新会议前,检查
using_堆顶。若其结束时间 \le 当前会议开始时间,则将其“移回”idle堆。 - 决策(Decision):
- 有空位:从
idle取出最小编号,会议按原计划开始。 - 无空位(核心创新点):从
using_强制取出最早结束的房间。关键逻辑:由于产生了等待,新会议的结束时间需要顺延(end = 房间空闲时间 + 会议持续时间)。
- 有空位:从
- 更新(Update):将房间重新放入
using_堆,并给该房间的计数器cnt[i]加一。
4. 复杂度与优势分析
- 性能:总时间复杂度为 O(M \log M + M \log N),空间复杂度为 O(N)。在处理大规模数据(如 10^5 级别)时具有极高的效率。
- 优势:
- 准确性:通过调整
end时间,完美模拟了会议延迟的物理过程。 - 简洁性:利用 C++ 现代特性(如
ranges和结构化绑定)使代码非常易读。
- 准确性:通过调整
总结:
这份分析深刻抓住了“状态转换(空闲 \leftrightarrow 使用)”这一核心,并解释了如何利用优先级队列来规避暴力搜索的低效。