会议室 II 图解题解
这道题到底在问什么
- 输入
- [[0,30],[5,10],[15,20],[6,35],[12,18],[25,40]]
- 输出
- 4
最优解:为什么这么做
一句话答案:按开始时间排序,用最小堆保存当前仍在进行的会议结束时间。每场开始前弹出所有 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),更像扫描线;堆写法则更直观地展示哪些会议仍活跃,也更容易扩展到房间分配明细。
▶ 动画逐步走查(共 37 步)——想跟着动画一帧帧对照就展开
- 3记住这条:排序定顺序,最小堆盯最早空出的房间——能复用就复用,不能就加房间。
- 4堆的全部本事就一句话:最小值在根、增删都只动一条路径上的几个节点。
- 5按开始时间排好序:[0,30] [5,10] [6,35] [12,18] [15,20] [25,40]。堆为空,开始逐场处理。
- 6第 1 场会议 [0,30]:当前没有房间,结束时间 30 放到堆末尾(下标 0),准备上浮。
- 730 就位,最小值仍浮在树根。
- 8第 1 场处理完:此刻同时占用 1 间,历史峰值 1。
- 9第 2 场会议 [5,10]:看堆顶 30 > 开始 5,最早的房间也还没散 → 必须新开一间。
- 10第 2 场会议 [5,10]:新开一间,结束时间 10 放到堆末尾(下标 1),准备上浮。
- 1110 比父节点 30 小,上浮一位 → 下标 0。
- 1210 上浮到下标 0 就位,堆顶仍是最早结束的房间。
- 13第 2 场处理完:此刻同时占用 2 间,历史峰值 2。
- 14第 3 场会议 [6,35]:看堆顶 10 > 开始 6,最早的房间也还没散 → 必须新开一间。
- 15第 3 场会议 [6,35]:新开一间,结束时间 35 放到堆末尾(下标 2),准备上浮。
- 1635 就位,最小值仍浮在树根。
- 17第 3 场处理完:此刻同时占用 3 间,历史峰值 3。
- 18第 4 场会议 [12,18]:看堆顶 10 ≤ 开始 12,这间房已散、可复用 → 弹出堆顶 10(这间房最早结束、现在腾出)。
- 19把末尾的 35 补到树根,准备下沉找回最小值。
- 2035 比孩子 30 大,下沉一位。
- 21下沉完成,最早结束的房间 30 回到树根。
- 22第 4 场会议 [12,18]:复用这间房,结束时间 18 放到堆末尾(下标 2),准备上浮。
- 2318 比父节点 30 小,上浮一位 → 下标 0。
- 2418 上浮到下标 0 就位,堆顶仍是最早结束的房间。
- 25第 4 场处理完:此刻同时占用 3 间,历史峰值 3。
- 26第 5 场会议 [15,20]:看堆顶 18 > 开始 15,最早的房间也还没散 → 必须新开一间。
- 27第 5 场会议 [15,20]:新开一间,结束时间 20 放到堆末尾(下标 3),准备上浮。
- 2820 比父节点 35 小,上浮一位 → 下标 1。
- 2920 上浮到下标 1 就位,堆顶仍是最早结束的房间。
- 30第 5 场处理完:此刻同时占用 4 间,历史峰值 4。
- 31第 6 场会议 [25,40]:看堆顶 18 ≤ 开始 25,这间房已散、可复用 → 弹出堆顶 18(这间房最早结束、现在腾出)。
- 32把末尾的 35 补到树根,准备下沉找回最小值。
- 3335 比最小的孩子 20 大,下沉一位。
- 34下沉完成,最早结束的房间 20 回到树根。
- 35第 6 场会议 [25,40]:复用这间房,结束时间 40 放到堆末尾(下标 3),准备上浮。
- 3640 就位,最小值仍浮在树根。
- 37第 6 场处理完:此刻同时占用 4 间,历史峰值 4。
- 40抓住「峰值 = 最大重叠」这层抽象,这道题就从「会议室」推广到一切「调度抢资源」。
- 46把它当模板:起点排序 → 起点前释放 → 占用 → 刷新峰值,换皮的调度题都能套。
⚠️ 容易写错的地方
✗ 错:存开始时间进堆
✓ 对:堆里存的是结束时间
要判断的是「房间何时空出」
✗ 错:比的是最后剩几间
✓ 对:答案是过程中的峰值
后面会议少不代表前面没挤过
✗ 错:相接当成重叠
✓ 对:heap[0] <= s 即可复用
上一场 10 结束、这场 10 开始不冲突
完整代码(Python / Java / C++)
Python
import heapq
class Solution:
def minMeetingRooms(self, intervals):
if not intervals: return 0
intervals.sort(key=lambda x: x[0]) # 按开始时间排序
heap = [] # 存各房间的结束时间(最小堆)
for s, e in intervals:
if heap and heap[0] <= s: # 最早空出的房间已散
heapq.heappop(heap) # 复用:腾出它
heapq.heappush(heap, e) # 占用到 e
return len(heap) # 堆大小的峰值 = 最少房间数Java
class Solution {
public int minMeetingRooms(int[][] intervals) {
if (intervals.length == 0) return 0;
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // 按开始时间排序
PriorityQueue<Integer> heap = new PriorityQueue<>(); // 最小堆:结束时间
for (int[] it : intervals) {
if (!heap.isEmpty() && heap.peek() <= it[0]) // 最早空出的房间已散
heap.poll(); // 复用:腾出它
heap.offer(it[1]); // 占用到结束时间
}
return heap.size(); // 堆大小的峰值 = 最少房间数
}
}C++
class Solution {
public:
int minMeetingRooms(vector<vector<int>>& intervals) {
if (intervals.empty()) return 0;
sort(intervals.begin(), intervals.end()); // 按开始时间排序
priority_queue<int, vector<int>, greater<int>> heap; // 最小堆
for (auto& it : intervals) {
if (!heap.empty() && heap.top() <= it[0]) // 最早空出的房间已散
heap.pop(); // 复用:腾出它
heap.push(it[1]); // 占用到结束时间
}
return heap.size(); // 堆大小的峰值 = 最少房间数
}
};复杂度
时间
O(n log n)
排序 O(n log n) + 每场会议一次堆操作 O(log n)
空间
O(n)
堆最多同时装下全部 n 个会议的结束时间
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 会议室 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用堆能做吗?+
能。把所有开始、结束时间拆成事件按时间排序,扫描时遇开始 +1、遇结束 -1,过程中的最大值就是答案,同样 O(n log n)。
堆解和扫描线解什么关系?+
本质都在求「最大同时重叠数」。堆解把「正在占用的结束时间」维护在堆里、堆 size 就是当前重叠数;扫描线用计数器。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 会议室 II 一步步讲透(全站已上线 948 份视频讲解,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。