(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] 误判为冲突)
核心洞察 会议室需求 = 同时进行的最大会议数
💡 扩展思考 若需返回具体分配方案?→ 需记录每间会议室的使用历史
📌 面试提示 优先讲解优先队列法(直观),再提及其他方法展示思维广度

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

相关推荐
Jason_Honey22 小时前
【平安Agent算法岗面试-二面】
人工智能·算法·面试
程序员酥皮蛋2 小时前
hot 100 第三十五题 35.二叉树的中序遍历
数据结构·算法·leetcode
追随者永远是胜利者2 小时前
(LeetCode-Hot100)207. 课程表
java·算法·leetcode·go
仰泳的熊猫3 小时前
题目1535:蓝桥杯算法提高VIP-最小乘积(提高型)
数据结构·c++·算法·蓝桥杯
那起舞的日子3 小时前
动态规划-Dynamic Programing-DP
算法·动态规划
yanghuashuiyue3 小时前
lambda+sealed+record
java·开发语言
闻缺陷则喜何志丹4 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
每天吃饭的羊4 小时前
时间复杂度
数据结构·算法·排序算法
盟接之桥4 小时前
盟接之桥EDI软件:API数据采集模块深度解析,打造企业数据协同新引擎
java·运维·服务器·网络·数据库·人工智能·制造