区间


目录:

预计阅读时间:5 分钟

区间

区间题看着花样多,拆开来常常只剩一件事:把每个区间拆成「开始」和「结束」两个事件,按时间扫一遍。本合集收录区间类题目。

资源数 = 最大重叠数

leetcode 253

给一组会议 [start, end],求最少需要几间会议室。

我的第一版:排序 + 堆,模拟分配。 按开始时间排序,堆里放每间会议室什么时候空出来。新会议到来时看最早空出来的那间:能用就复用,不能用就新开。11 分钟 AC:

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        hp = []
        intervals.sort()
        for s, e in intervals:
            if hp and s >= hp[0]:
                heapq.heappop(hp)
            heapq.heappush(hp, e)
        return len(hp)

(原版两个分支都 push,ans 和 len(hp) 始终相等,这里压成了三行。)

关键观察:答案就是同一时刻最多有几个会议在开。

  • 至少要这么多:某一刻有 k 个会议同时进行,它们各占一间,至少 k 间。
  • 这么多一定够:按开始时间处理,有空房就复用,没有才新开。新开那一刻所有已开的房都被占着,所以开出来的间数就是当时的重叠数,不会超过最大重叠数。

想通这一点,题目就从「分配房间」变成了「数重叠」:会议室彼此没有区别,谁进哪间根本不重要。堆其实在回答一个用不着问的问题。

扫描线:只数数,不分配。 每个会议拆成 +1(开始)和 -1(结束)两个事件,按时间扫一遍,记录进行中的会议数最多到过多少。开始时间和结束时间分别排序,就是两条已排好序的事件流,用双指针按时间合并:

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        starts = sorted(s for s, _ in intervals)
        ends = sorted(e for _, e in intervals)
        rooms = j = 0
        for s in starts:
            if s < ends[j]:    # 还没有会议结束 -> 新开一间
                rooms += 1
            else:              # 最早结束的已经走了 -> 复用
                j += 1
        return rooms

开始和结束拆开各自排序后,就对不上原来是哪个会议了,为什么还对?因为我们只关心「此刻有没有人刚走」,不关心是谁走的。

差分写法。 坐标范围小的话,直接开数组 d[s] += 1, d[e] -= 1,再求前缀和,就得到每个时刻有几个会议在开。坐标到 1e9 开不了数组,就只保留出现过的坐标,排序后再求前缀和——这就是扫描线:扫描线就是稀疏版的差分。

d = Counter()
for s, e in intervals:
    d[s] += 1
    d[e] -= 1
return max(accumulate(d[t] for t in sorted(d)), default=0)

端点相接是唯一的坑。 [1,5] 接 [5,8] 可以共用一间,同一坐标要先算结束、再算开始:双指针里写 s < ends[j],不能写 <=;差分里同一坐标的 +1 和 -1 落在同一个桶里,先抵消掉了。随机对拍 5000 组,三种写法和暴力全部一致;把 < 改成 <= 后错了 730 组。

复杂度:三种写法都是时间 $O(n \log n)$、空间 $O(n)$,瓶颈在排序。

可以迁移的问法。 只要题目问「最少需要多少资源,才能同时容纳所有区间」(会议室、站台、车辆容量、CPU),答案就是最大重叠数,用扫描线或差分来数,不必真的分配:

题 问法 做法
253 / 2406 最少资源 / 最少分组 最大重叠数
1094 容量够不够 差分,任一时刻超过容量即 false
1109 / 370 区间加后求数组 差分 + 前缀和
732 动态加区间,求最大重叠 有序 map 差分,或线段树
2402 要知道每个会议进了哪间 这时才真要分配:两个堆

本文由 aboom 原创,转载请注明出处。

📖相关推荐