会议室 II 图解题解
这道题到底在问什么
- 输入
- [[0,30],[5,10],[15,20],[6,35],[12,18],[25,40]]
- 输出
- 4
最优解:为什么这么做
一句话答案:LeetCode 253 会议室 II 的经典解是排序加最小堆:会议按开始时间排序,堆里存每间被占用房间的结束时间;每来一场会议先看堆顶——最早空出的那间若已在开始前散会(heap[0] ≤ s)就弹出复用,否则新开一间,最后堆的大小就是最少会议室数。时间 O(n log n)、空间 O(n)。
会议室 II 求的到底是什么
给一组会议的开始、结束时间,问最少要几间会议室才能让所有会议互不冲突。换个说法:在时间轴上,这些区间最多有几个同时叠在一起——重叠的会议必须各占一间,所以答案就是「最大同时重叠数」。抓住这层抽象,这道题就从会议室推广到一切「同时抢资源」的调度问题:站台数、服务器并发数,本质都一样。
为什么会议室 II 想到用最小堆
直觉的贪心是:会议按开始时间排好序逐场安排,能塞进已有房间就复用,实在不行才新开一间。这个贪心的关键子问题是——当前会议开始时,有没有房间已经空出来了?逐间检查所有房间要 O(n),太慢。
观察到只需要问一间房:所有占用中的房间里「最早结束」的那间。它都还没散会,其他房间更没散,必须新开;它散了,就复用它。「反复取最小值、且元素动态进出」正是最小堆(优先队列)的看家本领:堆顶永远是最小值,插入删除都只动一条路径,各 O(log n)。于是把每间房的结束时间丢进最小堆,堆顶就是那间最早空出的房。
每一步的复用与新开为什么都对
会议 [s, e] 到来时比较堆顶:heap[0] ≤ s,说明最早散会的房间在这场会开始前(或恰好在开始那一刻)已腾出,弹出堆顶再压入 e——一出一进,堆大小不变,对应「复用」;heap[0] > s,说明现有房间全在忙,直接压入 e,堆大小加一,对应「新开一间」。注意相接不算冲突:上一场 10 点结束、这一场 10 点开始,同一间房接得上,所以判断带等号。
堆大小只在「所有房都忙」时才增长,而那一刻的堆大小恰好等于当时同时进行的会议数。又因为按开始时间排序,每场会议在它开始的时刻被处理,重叠计数不会漏算。所以最终的堆大小 = 过程中同时占用的峰值 = 最少会议室数,参考代码最后直接 return len(heap)。
不用堆的扫描线解法是什么关系
把每场会议拆成两个事件:开始时刻 +1、结束时刻 -1,全部事件按时间排序后扫一遍,计数器的历史最大值就是答案,这就是扫描线做法,同样 O(n log n)。它和堆解殊途同归:都在数「最大同时重叠」,堆解用堆的实时大小当计数器,扫描线用显式加减。面试里能把两种解法的等价性讲清楚,是很好的加分点。
复杂度与最容易错的三个点
时间 O(n log n):排序 O(n log n),之后每场会议至多一次弹出、一次压入,各 O(log n)。空间 O(n):最坏所有会议全部重叠,堆里同时装下 n 个结束时间。
三个高频错误:一是往堆里存开始时间——要判断的是「房间何时空出」,必须存结束时间;二是把答案理解成「扫完后还剩几间在用」,其实要的是过程中的峰值(本解法堆大小不会缩,天然记住了峰值);三是把端点相接当成冲突,漏掉 heap[0] ≤ s 里的等号,会多开无谓的房间。
▶ 动画逐步走查(共 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 补到树根,准备下沉找回最小值。
- 2030 比孩子 35 大,下沉一位。
- 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 补到树根,准备下沉找回最小值。
- 3320 比孩子 35 大,下沉一位。
- 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
def minMeetingRooms(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
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++
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 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。