LC56 合并区间¶
讲解章节:排序
一句话
按左端点排序后一趟扫描,能接上就把当前区间的右端点往外撑。
解题思路¶
这题考什么¶
两个区间 [a1, b1] 与 [a2, b2] 有交叠的条件是 a1 <= b2 且 a2 <= b1, 在乱序的输入上判断任意两个是否重叠要 O(n^2) 对,而且合并出的新区间还可能 与之前已经排除过的区间重新产生交叠,得反复扫。
先按左端点升序排序,这个麻烦就消失了。排序之后有一条关键性质:
扫到第 i 个区间时,它的左端点不小于前面所有区间的左端点。
于是它要么与「当前正在攒的那个合并区间」相交(左端点落在其内部或右端点上),
要么整个落在它右边——**不可能与更早的、已经封存的区间再产生交叠**,
因为那些区间的右端点只会更靠左。
所以只需要维护「最后一个已输出的区间」,每来一个新区间做二选一:
这是典型的排序 + 贪心一趟扫(见 排序 与 贪心),与区间调度、活动选择是同一族问题。 注意那一族常按右端点排序(要选最多的不相交区间),本题按左端点排序, 因为要的是把相邻的连成一片,得让扫描顺序与区间在数轴上的先后一致。
另一条路是差分/扫描线(见 前缀和与差分): 把左端点记 +1、右端点记 -1,排序后累加,计数从 0 变正的位置是一段的开头、 回到 0 的位置是结尾。它能处理更一般的「覆盖次数」问题,但本题只要合并, 上面的一趟扫更短也更直观;而且端点闭合语义([1,4] 与 [4,5] 算相接) 在差分写法里需要额外约定同一坐标上 +1 先于 -1,反而容易出错。
数据规模与复杂度¶
1 <= n <= 1e4,区间端点满足 0 <= start_i <= end_i <= 1e4。 时间 O(n log n),瓶颈在排序;扫描本身是 O(n)。空间 O(n) 存答案 (Python 的 list.sort 是原地的 Timsort,排序本身不额外占 O(n))。 两两比较合并的朴素做法是 O(n^2) = 1e8 对,还要反复迭代到不动点,Python 下太慢。
坑在哪¶
- 参数是二维数组,不是区间对象。每个区间就是长度 2 的 list,
用
intervals[i][0]/intervals[i][1]取端点。牛客 BM89 的同名题给的是Interval类、要写.start/.end,两边的代码不能直接搬。 - 相接算重叠,判据必须带等号:
start <= ans[-1][1]。用严格小于的话, 样例 2 的 [1,4] 与 [4,5] 会被当成两段输出 [[1,4],[4,5]],而期望是 [[1,5]]。 - 扩右端点要取
max,不能无脑赋值。[[1,10],[2,3]] 排序后仍是这个顺序, 若直接把右端点写成 3,就会把 [1,10] 缩成 [1,3]——包含关系的区间必须留住外层的。 - 追加时要
ans.append([start, end])新建一个 list, 不要把输入的那一行直接挂进答案。挂进去之后第 3 步的扩端点会顺手改写入参, 若调用方之后还要用原数组就会读到被污染的数据。 - 排序键只取左端点即可。左端点相同时右端点的先后不影响结果—— 反正下一步就会被 max 撑到两者的较大值。
- 输入不保证有序,样例 3 的 [[4,7],[1,4]] 就是乱的。省掉排序直接扫, 它会被当成两段互不相交的区间输出,而期望是 [[1,7]]。
- 严格比对:排序后一趟扫产生的结果天然按左端点升序,与力扣的期望输出顺序一致, 不需要特判顺序(本题在 meta.json 的 judge 里没有配 unordered)。
样例复核¶
intervals = [[1,3],[2,6],[8,10],[15,18]],期望 [[1,6],[8,10],[15,18]]。 排序后不变。逐个扫:
[1,3] 答案为空,直接追加 ans = [[1,3]]
[2,6] 2 <= 3,相交,右端点 max(3, 6) = 6 ans = [[1,6]]
[8,10] 8 > 6,断开,追加 ans = [[1,6], [8,10]]
[15,18] 15 > 10,断开,追加 ans = [[1,6], [8,10], [15,18]]
与样例一致。样例 3 的 [[4,7],[1,4]] 排序后变成 [[1,4],[4,7]], 第二步 4 <= 4 成立(等号在这里救了场),合并成 [1,7],同样一致。