LEETCODE 253Medium

会议室 II

把开始时间和结束时间拆开各自排序,双指针推进:某场会议开始时还没有任何会议结束,就必须新开一间房。

问题拆解

给一批会议的起止时间,问最少需要多少间会议室才能让所有会议不冲突地开完。答案其实是一个瞬时量:某一时刻同时进行的会议有几场,那一刻就需要几间房;全天的最大并发数就是最终答案。

朴素做法是模拟分配:会议按开始时间排序,每来一场就在已有房间里找一间“上一场已结束”的,找不到就新开。直接实现要对每场会议扫一遍所有房间,O(n²);用最小堆维护各房间的最早结束时间可以降到 O(n log n),堆的大小峰值就是答案。

但有一个更轻的观察:我们只关心“需要几间房”,不关心“哪场会议进哪间房”。把所有开始时间和所有结束时间拆成两个独立的序列分别排序——排序打乱了起止时间的配对关系,可这不影响并发数的统计:每个开始事件让并发数加一,每个结束事件让它减一,事件发生的顺序只由时间决定。

下一场会议的开始时间早于当前最早的结束时间,说明没有任何房间来得及腾出来,只能加一间;否则就复用刚结束的那间。房间数只增不减,因为我们要的是峰值。

起止时间分离排序 + 双指针

public int minMeetingRooms(int[][] intervals) {
    int n = intervals.length;
    int[] starts = new int[n];
    int[] ends = new int[n];
    for (int i = 0; i < n; i++) {
        starts[i] = intervals[i][0];
        ends[i] = intervals[i][1];
    }
    Arrays.sort(starts);
    Arrays.sort(ends);
    int rooms = 0, e = 0;
    for (int s = 0; s < n; s++) {
        if (starts[s] < ends[e]) {
            rooms++; // 开始时没有房间腾出,新开一间
        } else {
            e++; // 复用最早结束的房间
        }
    }
    return rooms;
}
def minMeetingRooms(intervals: list[list[int]]) -> int:
    starts = sorted(s for s, _ in intervals)
    ends = sorted(e for _, e in intervals)
    rooms = e = 0
    for s in starts:
        if s < ends[e]:
            rooms += 1  # 开始时没有房间腾出,新开一间
        else:
            e += 1  # 复用最早结束的房间
    return rooms
func minMeetingRooms(intervals [][]int) int {
    n := len(intervals)
    starts := make([]int, n)
    ends := make([]int, n)
    for i, iv := range intervals {
        starts[i] = iv[0]
        ends[i] = iv[1]
    }
    slices.Sort(starts)
    slices.Sort(ends)
    rooms, e := 0, 0
    for _, s := range starts {
        if s < ends[e] {
            rooms++ // 开始时没有房间腾出,新开一间
        } else {
            e++ // 复用最早结束的房间
        }
    }
    return rooms
}
pub fn min_meeting_rooms(intervals: Vec<Vec<i32>>) -> i32 {
    let mut starts: Vec<i32> = intervals.iter().map(|iv| iv[0]).collect();
    let mut ends: Vec<i32> = intervals.iter().map(|iv| iv[1]).collect();
    starts.sort_unstable();
    ends.sort_unstable();
    let mut rooms = 0;
    let mut e = 0;
    for &s in &starts {
        if s < ends[e] {
            rooms += 1; // 开始时没有房间腾出,新开一间
        } else {
            e += 1; // 复用最早结束的房间
        }
    }
    rooms as i32
}

指针 s 走开始序列,e 停在“最早的尚未被复用的结束时间”上。每场会议开始时二选一:starts[s] < ends[e] 就加房间,否则把 e 前移表示复用。最容易错的是边界比较:starts[s] == ends[e] 时必须走复用分支——一场会议 10 点结束、另一场 10 点开始,同一间房交接即可。把 < 写成 <= 会多算一间。另外 e 不会越界:复用一次消耗一个结束事件,而结束事件和开始事件一样多。

顺带一提最小堆做法:会议按开始时间排序,堆里存各房间的结束时间,堆顶结束得早于当前会议开始就弹出复用,历史堆大小的峰值即答案——思路等价,代码略长,但能顺便回答“每场会议分在哪间房”。

复杂度

指标 复杂度 原因
时间 O(n log n) 两次排序占主导,双指针扫描是线性的
空间 O(n) 拆出的两个时间数组

可以迁移的模式

  • 求“最少资源数”时先翻译成“最大并发数”,答案是峰值而不是过程;
  • 当问题只关心数量、不关心配对关系时,大胆拆散区间的起止端点分别排序;
  • 起点与终点相等时算不算重叠,是所有区间题都要先跟题意对齐的边界。

这套“事件化 + 排序扫描”的手法同样适用于机场停机位、地铁最大在车人数等一切求峰值并发的问题。