题目描述
思路解析
一句话答案:按开始时间排序,用最小堆保存当前仍在进行的会议结束时间。每场开始前弹出所有 end≤start 的会议,再加入当前结束时间;堆大小是真实并发数,维护其历史最大值就是最少会议室数。
本质是求最大重叠数
如果某一时刻有 k 场会议同时进行,少于 k 间会议室一定不够;反过来,只要每场开始前复用所有已经空出的房间,峰值为 k 时就总能用 k 间完成安排。因此最少房间数等于时间线上的最大并发会议数。
这解释了为什么不能只看最后还剩多少场:高峰可能出现在中间,后面的会议变少并不会降低曾经必须准备的房间数。
最小堆维护当前活跃会议
先按 start 升序处理会议。堆里只存尚未结束会议的 end,堆顶是最早结束时间。处理 [start,end] 前,只要 heap[0]≤start,就说明该会议已结束,应继续弹出。
释放完成后压入 end。此时堆中每个元素都对应 start 时刻仍在进行的一场会议,因此堆大小具有明确含义:当前真实并发数。用 best 取全程最大值即可。
为什么必须是 while,而不是 if
相邻两次开始之间可能结束多场会议。例如活跃结束时间是 [18,20,30,35],新会议从 25 开始,18 和 20 都已经结束。若只弹一次,20 会错误残留在堆里,堆大小就不再等于当前并发。
经典的“只弹一个、最终返回堆大小”写法也可以求最少房间数,但那时堆表示已分配房间的最新结束时间集合,而不是当前活跃会议。教学中不能把两套语义混在一起。
正确性与端点约定
按开始时间推进时,while 会恰好删除所有在当前开始时间之前或同时结束的会议,留下的会议都与当前会议重叠。加入当前会议后,堆大小因此等于该时刻需要的房间数;best 覆盖所有开始时刻,也就覆盖全部可能出现新峰值的时刻。
释放条件必须是 end≤start。会议 [0,10] 与 [10,20] 首尾相接,不发生重叠,可以使用同一间房;写成 end<start 会多算一间。
复杂度与扫描线替代方案
排序需要 O(n log n)。每个结束时间最多入堆一次、出堆一次,每次 O(log n),总时间仍为 O(n log n);最坏全部重叠时堆空间 O(n)。
另一种做法是分别排序 start 与 end,用双指针维护当前并发和峰值。它同样是 O(n log n),更像扫描线;堆写法则更直观地展示哪些会议仍活跃,也更容易扩展到房间分配明细。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
每场会议都做三件事:释放所有已结束会议、加入当前结束时间、用当前堆大小刷新并发峰值。
排序后依次处理 [0,30]、[5,10]、[6,35]、[12,18]、[15,20]、[25,40];堆初始为空。
第一场会议开始前堆为空,没有任何结束时间可释放;先明确检查释放条件,再执行压入。
[0,30] 开始前没有活跃会议;压入结束时间 30,当前并发为 1,峰值更新为 1。
第二场从 5 开始,最早结束时间 30 仍在未来,因此没有房间空出;新会议会让并发从 1 变为 2。
压入结束时间 10 后,它比 30 更早,于是上浮到堆顶;下一场会议只需先看这个最小值。
[5,10] 到来时,最早结束的 30 仍大于 5;没有房间释放,压入 10 后并发变为 2。
第三场从 6 开始,而最早结束的会议也要到 10 才结束;现有两间都忙,只能增加一场活跃会议。
[6,35] 开始时堆顶 10 还未结束;压入 35 后有三场会议同时进行,峰值成为 3。
第四场从 12 开始,堆顶 10 已经结束。释放循环命中一次,先弹出 10,再考虑是否还有会议也已结束。
弹出 10 后新堆顶是 30,它仍大于 12,所以 while 停止;此刻堆里精确保留两场仍活跃的会议。
把当前结束时间 18 压入后,并发从 2 回到 3;18 成为新的最早结束时间,峰值没有超过先前的 3。
[12,18] 前先弹出 10,因为那场会议已经结束;再压入 18,真实活跃会议仍是 3 场。
第五场从 15 开始,18 还没有结束,因此三场旧会议都活跃;再加入当前会议就会首次达到 4 场并发。
[15,20] 开始时最早结束时间 18 仍大于 15;压入 20 后并发达到 4,形成全程峰值。
最后一场从 25 开始。堆顶 18 已结束,while 先弹它;关键是弹完后必须重新查看新的堆顶。
弹出 18 后,新堆顶 20 也不晚于 25。如果这里误写成 if 就会停下,堆中残留一场已结束会议。
继续弹出 20 后,堆顶变成 30;它仍在 25 之后结束,释放循环到此停止,活跃会议准确剩两场。
[25,40] 前,18 和 20 都已经结束,必须用 while 连续弹出;压入 40 后当前并发是 3,而峰值仍是 4。
两种写法都能算房间数,但语义不能混用;动画要展示真实并发,就必须释放所有已结束会议并维护峰值。
边界例子说明等号不能漏:上一场恰在 10 结束,新会议也从 10 开始,两者不重叠,可以复用同一间房。
空输入、首尾相接、全部重叠和“长会议包住多场短会议”能覆盖释放条件与峰值语义。
面试时要能区分活跃会议模型与房间分配模型,并说明它们为何都能求答案、语义却不同。
参考代码
import heapqclass Solution: def minMeetingRooms(self, intervals): intervals.sort(key=lambda x: x[0]) active = [] best = 0 for start, end in intervals: while active and active[0] <= start: heapq.heappop(active) heapq.heappush(active, end) best = max(best, len(active)) return best复杂度
- 时间:O(n log n),排序 O(n log n),每个结束时间最多入堆和出堆各一次
- 空间:O(n),最坏所有会议重叠,活跃堆保存 n 个结束时间
易错点
面试追问把动画讲成自己的话
追问能不用堆吗?
追问为什么最小堆只存结束时间?
追问只弹一个的经典写法错吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
包含每个查询的最小区间
LeetCode 1851 · 困难 · 沿着 区间 继续往下推进