(LeetCode-Hot100)253. 会议室 II

问题简介

LeetCode 253. 会议室 II

复制代码
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

题目描述

给你一个会议时间安排的数组 intervals,其中 intervals[i] = [starti, endi] 表示会议 i 的开始和结束时间,请你返回至少需要多少间会议室


示例说明

示例 1:

复制代码
输入: intervals = [[0,30],[5,10],[15,20]]
输出: 2
解释: 
- 会议 [0,30] 占用会议室 1
- 会议 [5,10] 与 [0,30] 冲突,需会议室 2
- 会议 [15,20] 可复用会议室 2(因为 [5,10] 已结束)

示例 2:

复制代码
输入: intervals = [[7,10],[2,4]]
输出: 1
解释: 两个会议时间不重叠,可共用一间会议室。

解题思路

💡 方法一:优先队列(最小堆)------ 推荐解法

核心思想 :按会议开始时间排序,用最小堆维护当前所有会议室的结束时间。当新会议到来时:

  • 如果最早结束的会议已结束(堆顶 ≤ 当前会议开始时间),则复用该会议室(弹出堆顶,压入新结束时间)
  • 否则,新开一间会议室(直接压入新结束时间)

步骤详解

  1. 将所有会议按 start 升序排序
  2. 初始化最小堆(存储会议结束时间)
  3. 遍历每个会议:
    • 若堆非空且堆顶 ≤ 当前会议 start → 弹出堆顶(释放会议室)
    • 将当前会议 end 压入堆
  4. 最终堆大小即为所需会议室数

💡 方法二:上下车模型(扫描线)

核心思想:将每个会议拆分为两个事件:

  • (start, +1):上车(需要会议室)
  • (end, -1):下车(释放会议室)

按时间排序所有事件,遍历时累加变化值,最大累加值即为答案。

关键细节 :若时间相同,先处理下车事件(-1),再处理上车事件(+1),避免多算。

💡 方法三:双指针(分离起点终点)

核心思想

  1. 分别提取所有 startend 时间并排序
  2. 用双指针遍历:
    • start[i] < end[j] → 需要新会议室(count++, i++
    • 否则 → 释放会议室(j++
  3. 记录 count 的最大值

代码实现

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<>();
        heap.offer(intervals[0][1]);
        
        for (int i = 1; i < intervals.length; i++) {
            // 如果最早结束的会议已结束,复用会议室
            if (heap.peek() <= intervals[i][0]) {
                heap.poll();
            }
            heap.offer(intervals[i][1]);
        }
        return heap.size();
    }
}

// 方法二:上下车模型
class Solution {
    public int minMeetingRooms(int[][] intervals) {
        List<int[]> events = new ArrayList<>();
        for (int[] interval : intervals) {
            events.add(new int[]{interval[0], 1});  // 上车
            events.add(new int[]{interval[1], -1}); // 下车
        }
        
        // 排序:时间相同时,先下车(-1)后上车(1)
        events.sort((a, b) -> {
            if (a[0] != b[0]) return a[0] - b[0];
            return a[1] - b[1];
        });
        
        int count = 0, maxRooms = 0;
        for (int[] event : events) {
            count += event[1];
            maxRooms = Math.max(maxRooms, count);
        }
        return maxRooms;
    }
}
go:Go 复制代码
// 方法一:优先队列(最小堆)
import "sort"

type MinHeap []int

func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x interface{}) { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

func minMeetingRooms(intervals [][]int) int {
    if len(intervals) == 0 {
        return 0
    }
    
    // 按开始时间排序
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][0] < intervals[j][0]
    })
    
    // 最小堆存储结束时间
    heap := &MinHeap{intervals[0][1]}
    for i := 1; i < len(intervals); i++ {
        // 如果最早结束的会议已结束,复用会议室
        if (*heap)[0] <= intervals[i][0] {
            heap.Pop()
        }
        heap.Push(intervals[i][1])
    }
    return heap.Len()
}

// 方法二:上下车模型
func minMeetingRooms(intervals [][]int) int {
    type Event struct {
        time int
        diff int // +1 上车, -1 下车
    }
    
    events := make([]Event, 0, len(intervals)*2)
    for _, interval := range intervals {
        events = append(events, Event{interval[0], 1})
        events = append(events, Event{interval[1], -1})
    }
    
    // 排序:时间相同时,先下车(-1)后上车(1)
    sort.Slice(events, func(i, j int) bool {
        if events[i].time != events[j].time {
            return events[i].time < events[j].time
        }
        return events[i].diff < events[j].diff
    })
    
    count, maxRooms := 0, 0
    for _, e := range events {
        count += e.diff
        if count > maxRooms {
            maxRooms = count
        }
    }
    return maxRooms
}

示例演示

📌 [[0,30],[5,10],[15,20]] 为例(方法一)

步骤 当前会议 堆状态(结束时间) 操作 会议室数
初始 - [30] 加入 [0,30] 1
1 [5,10] [10, 30] 5 < 30 → 新开会议室 2
2 [15,20] [20, 30] 15 ≥ 10 → 复用(弹出10) 2

✅ 最终结果:2


答案有效性证明

✅ 方法一正确性

  • 贪心选择:总是复用最早结束的会议室,保证资源最优利用
  • 数学归纳 :假设前 k 个会议已最优分配,第 k+1 个会议若能复用则必复用(否则浪费资源),若不能则必须新开

✅ 方法二正确性

  • 事件离散化:将连续时间转化为离散事件点
  • 差分思想+1/-1 精确模拟资源占用变化
  • 排序规则 :时间相同时先释放再占用,避免瞬时多占(如 [1,2],[2,3] 只需1间)

复杂度分析

方法 时间复杂度 空间复杂度 说明
优先队列 O(n log n) O(n) 排序 O(n log n) + 堆操作 O(n log n)
上下车模型 O(n log n) O(n) 排序 2n 个事件
双指针 O(n log n) O(n) 排序两个数组

💡 最优实践:优先队列法最直观,上下车模型代码更简洁,双指针空间常数更小。


问题总结

关键点 说明
常见错误 忽略时间相同时的处理顺序(如 [1,2],[2,3] 误判为冲突)
核心洞察 会议室需求 = 同时进行的最大会议数
💡 扩展思考 若需返回具体分配方案?→ 需记录每间会议室的使用历史
📌 面试提示 优先讲解优先队列法(直观),再提及其他方法展示思维广度

一句话总结"按开始时间排序,用最小堆跟踪最早空闲的会议室" 是解决区间调度类问题的经典范式。

相关推荐
曹牧8 小时前
BeanUtils.copyProperties‌
java
QWQ___qwq8 小时前
Java线程安全深度总结:基本类型与引用类型的本质区别
java·安全·面试
识君啊9 小时前
Java异常处理:中小厂面试通关指南
java·开发语言·面试·异常处理·exception·中小厂
qyzm11 小时前
天梯赛练习(3月13日)
开发语言·数据结构·python·算法·贪心算法
月月玩代码11 小时前
Actuator,Spring Boot应用监控与管理端点!
java·spring boot·后端
逆境不可逃11 小时前
LeetCode 热题 100 之 64. 最小路径和 5. 最长回文子串 1143. 最长公共子序列 72. 编辑距离
算法·leetcode·动态规划
feng一样的男子11 小时前
NFS 扩展属性 (xattr) 提示操作不支持解决方案
linux·go
CoderCodingNo11 小时前
【GESP】C++五级练习题 luogu-P1182 数列分段 Section II
开发语言·c++·算法
放下华子我只抽RuiKe511 小时前
机器学习全景指南-直觉篇——基于距离的 K-近邻 (KNN) 算法
人工智能·gpt·算法·机器学习·语言模型·chatgpt·ai编程
kisshuan1239611 小时前
[特殊字符]【深度学习】DA3METRIC-LARGE单目深度估计算法详解
人工智能·深度学习·算法