题目描述
思路解析
一句话答案: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 里的等号,会多开无谓的房间。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条:排序定顺序,最小堆盯最早空出的房间——能复用就复用,不能就加房间。
堆的全部本事就一句话:最小值在根、增删都只动一条路径上的几个节点。
按开始时间排好序:[0,30] [5,10] [6,35] [12,18] [15,20] [25,40]。堆为空,开始逐场处理。
第 1 场会议 [0,30]:当前没有房间,结束时间 30 放到堆末尾(下标 0),准备上浮。
30 就位,最小值仍浮在树根。
第 1 场处理完:此刻同时占用 1 间,历史峰值 1。
第 2 场会议 [5,10]:看堆顶 30 > 开始 5,最早的房间也还没散 → 必须新开一间。
第 2 场会议 [5,10]:新开一间,结束时间 10 放到堆末尾(下标 1),准备上浮。
10 比父节点 30 小,上浮一位 → 下标 0。
10 上浮到下标 0 就位,堆顶仍是最早结束的房间。
第 2 场处理完:此刻同时占用 2 间,历史峰值 2。
第 3 场会议 [6,35]:看堆顶 10 > 开始 6,最早的房间也还没散 → 必须新开一间。
第 3 场会议 [6,35]:新开一间,结束时间 35 放到堆末尾(下标 2),准备上浮。
35 就位,最小值仍浮在树根。
第 3 场处理完:此刻同时占用 3 间,历史峰值 3。
第 4 场会议 [12,18]:看堆顶 10 ≤ 开始 12,这间房已散、可复用 → 弹出堆顶 10(这间房最早结束、现在腾出)。
把末尾的 35 补到树根,准备下沉找回最小值。
30 比孩子 35 大,下沉一位。
下沉完成,最早结束的房间 30 回到树根。
第 4 场会议 [12,18]:复用这间房,结束时间 18 放到堆末尾(下标 2),准备上浮。
18 比父节点 30 小,上浮一位 → 下标 0。
18 上浮到下标 0 就位,堆顶仍是最早结束的房间。
第 4 场处理完:此刻同时占用 3 间,历史峰值 3。
第 5 场会议 [15,20]:看堆顶 18 > 开始 15,最早的房间也还没散 → 必须新开一间。
第 5 场会议 [15,20]:新开一间,结束时间 20 放到堆末尾(下标 3),准备上浮。
20 比父节点 35 小,上浮一位 → 下标 1。
20 上浮到下标 1 就位,堆顶仍是最早结束的房间。
第 5 场处理完:此刻同时占用 4 间,历史峰值 4。
第 6 场会议 [25,40]:看堆顶 18 ≤ 开始 25,这间房已散、可复用 → 弹出堆顶 18(这间房最早结束、现在腾出)。
把末尾的 35 补到树根,准备下沉找回最小值。
20 比孩子 35 大,下沉一位。
下沉完成,最早结束的房间 20 回到树根。
第 6 场会议 [25,40]:复用这间房,结束时间 40 放到堆末尾(下标 3),准备上浮。
40 就位,最小值仍浮在树根。
第 6 场处理完:此刻同时占用 4 间,历史峰值 4。
抓住「峰值 = 最大重叠」这层抽象,这道题就从「会议室」推广到一切「调度抢资源」。
空、单场、首尾相接、全重叠四个端点都要照顾到——逻辑不变,堆自然处理。
堆解和扫描线解是一体两面:都在求最大重叠,只是维护当前重叠数的方式不同。
把它当模板:起点排序 → 起点前释放 → 占用 → 刷新峰值,换皮的调度题都能套。
参考代码
import heapqdef 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) # 堆大小的峰值 = 最少房间数复杂度
- 时间:O(n log n),排序 O(n log n) + 每场会议一次堆操作 O(log n)
- 空间:O(n),堆最多同时装下全部 n 个会议的结束时间
易错点
面试追问把动画讲成自己的话
追问不用堆能做吗?
追问堆解和扫描线解什么关系?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
包含每个查询的最小区间
LeetCode 1851 · 困难 · 沿着 区间 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题