7/5, Interval 类,扫描线
7/5, Interval 类,扫描线
Interval 类问题中最常用的技巧,就是自定义 IntervalComparator,把输入按照 startTime 升序排序。
对于任意两个区间A与B,如果
A.end > B.start 并且
A.start < B.end
则 A 与 B 重叠。
按 start 排序后,数组有了单调性,上面的判断条件就简化成了 A.end > B.start 则一定重叠.
排序后的 Interval 扫描过程中,为了保证正确性,要格外小心多个 Interval 在同一 x 时,处理的先后顺序,比如 skyline problem.
熟悉下 TreeMap 的常用 API.
get()
put()
containsKey()
size()
values() : 返回 Collection<> of V
lowerKey(K key) : 返回最大的小于参数的Key,没有则 null
higherKey(K key):返回最小的大于参数的Key, 没有则 null
firstKey() : 返回最小 key
lastKey() : 返回最大 key
Trivial Problem.
fb 面经高频题,follow up 是返回那个 overlap 最多的区间。
另一个 fb onsite 面经题里,首先给 n 个一维 interval,返回任意重合最多的点;然后给 n 个二维 rectangle,返回任意重复最多的点。
扫描线算法。需要注意的是如果有两个 interval 首尾相接,要把结束的那个排在 array 的前面,先把房间腾出来;否则的话会认为收尾相接的两个 meeting 需要占 2 个房间,这是错误的。
(FB) follow-up: 如何返回重叠最多的那个区间?
当前 maxOverlap 创造新高的时候,存下 start 的时间戳,因为这是所有重合区间中 start 时间最晚的一个;
继续扫描,看到新的 end 的时候,存下这个 end 的时间戳,因为它是重合区间里 end 时间最早的一个;
二者之间,就是具体的 max overlap 区间。

针对这题还有另外一种写法,用 heap / PriorityQueue,相比之下代码量要小一点,不过不像扫描线算法那样清晰易于理解,而且容易进行修改适应各种 follow-up 情况。
这题需要注意的 corner case 有两个:
按 startTime 排序后, merge 的新 interval 可能是完全包含于前一个 interval 的,如 【1,4】,【2,3】. 所以当发现 overlap 时,新的 End 要以两个 end 的最大值为准。
记得循环尾部做一个收尾,把最后 merge 完的结果生成新的 interval 也加到 list 中。
顺着上一题的思路,一个最简单直接的写法就是。。直接把新的 interval 插进去,然后再跑一遍 merge interval 的代码。
考虑到一些极端情况,比如 newInterval 合并了原始 list 中绝大多数 intervals, 在原 list 上进行操作的话需要的比较与删减也会很多,所以从算法复杂度上讲,这种写法至少可以 AC,但不是最优。
时间复杂度 O(n log n ) + O(n)
如果要进行进一步优化,要利用原题中 set of "non-overlapping" intervals 的条件,所以 newInterval 有可能会与任何 interval 或 N 个 interval 的集合,在左右两边 merge [0, N] 个 interval, 从完全 disjoint 到完全合并。
时间复杂度 O(n),one-pass.
在此基础上还可以进一步优化,不用额外空间,on-the-fly 解决战斗。
这种做法可以不花费任何额外空间,但是时间复杂度会更高,因为 List.remove() 是一个 O(n) 操作,add(index, val) 也是 O(n) 的。
一个比较简单的区间融合问题。
第一种不太经济的写法是,建一个 boolean[upper - lower + 1] ,扫一遍数组记录下每个数是否出现过,然后根据 boolean[] 的 flag 进行融合。 这种写法首先需要 O(upper - lower) 的 extra space,而且最重要的是,没有利用到原数组 int[] 已经是排序了的性质,因此速度上不给力,而且在 upper = 100000000, lower = -10000000 这种极端情况下,空间占用非常大。
改进后的代码实现过程中,最开始没有考虑全 test case,虽然AC 了但是代码不够 clean. 下面参考论坛里的解法就简明了很多。
如果实现代码的时候发现要写很多 if else 处理特殊情况,最好的选择还是暂停一下,多把 test case 思考全再动手。
【lower, nums[i] - 1】 范围内没有数, ADD;
【nums[last] + 1,upper】 范围内没有数, ADD;
动态更新 lower,维护当前有效 range.
时间复杂度 O(n).
作为一个 data structure design 题,首先要问好到底哪个 API call 比较频繁,设计上倾向于优化哪个操作。
先写了个略粗糙的版本,维护当前所有 interval 的 list,每次插入操作的时候靠 binary search 找每个 val 的位置,然后分情况考虑。
addNum(int val) 时间复杂度 O(log n) + O(n)
getInterval() 时间复杂度 O(1)
空间复杂度 O(n)
n = 当前有效数字个数
在测试数据集上,这个方法和 TreeMap 的做法用时不相上下。
第二个版本,自己写了个 LinkedList 维护目前所有数字,每次 call getIntervals() 的时候根据 list 当场生成。
addNum(int val) 时间复杂度 O(n);
getIntervals() 时间复杂度 O(n);
空间:O(n);
n = 当前的有效数字个数。
参考了论坛代码之后,另一种巧妙的做法:
addNum(int val) 时间复杂度:O(log n)
getIntervals() 时间复杂度:o(n log n)
空间复杂度: O(n)
(FB) 给定的是 stream of intervals,求 cover range.
这么改了之后难了很多。。用 TreeMap 依然可以,但是假设目前 TreeMap 维护的都是 none-overlapping intervals,一个新 interval 加入之后可能会出现若干种情况:
完全被某个 interval 包含,无影响;
完全和其他 interval disjoint,插入即可;
和左边的 overlap,但是不和右边的 overlap;
和右边的 overlap,但是不和左边的 overlap;
一次跨了多个 interval,这里面就有全包括的,包括一半的,在左边搭边的,在右边搭边的。。。
于是我觉得这题用 TreeMap 比较和乎人性的做法是,TreeMap 只负责维护 sorted intervals,需要查询 coverage 的时候,直接 new ArrayList<>(treeMap.values()) 给导出来然后跑一遍 merge interval ..
或者,直接维护一个按照 start time 排好序的 disjoint list of intervals,然后每次新的 interval 过来的时候,就 in-place 扫一遍做 merge 好了~ 这样每次 insert O(N),getCoverage O(1)
扫描线算法,因为 building outline 只可能发生在每一段 "start/end" 的边界上,因此以每个 edge 的 (x, height) 排序,而后扫描。
当 x 值不同时,x 小的排在前面;
else 当 y 值不同时,y 小的排在前面;
调换的话下面的代码会有 bug
这题在九章课上讲的时候,使用的是 HashHeap,我们需要一个 maxHeap 来检测对于每一个点,我们建筑的最大高度,但同时在扫描过程中我们需要对每一个具体的 edge 进行删除操作,而 HashHeap 可以做到 O(log n) 的复杂度,Java 默认的 heap 只能做到 O(n).
如何在 heap 中快速删除这个问题和当初准备 Bloomberg 时候的马拉松问题一样,Java 中有现成的解决方案:
TreeMap.
接下来的问题是:
当遇到一座楼的右边界时,如何删除其对应的左边界?在 TreeMap 里直接用 Edge class 做 key 是不行的,因为我们无法通过 new 一个参数一模一样的 instance 去做 lookup. 所以在 Key-Value 的设计中,我们应该用 height 做 Key,因为同一座建筑左右墙 height 相等,就可以实现查找。即使有多座 height 一样的左墙,我们也只需要用 count 做 value,把当前 height 的墙数减一即可。
对于同一高度的不同建筑,如何确保其在 TreeMap 中不会互相 replace ,从而保证算法的正确性? 在自己 debug 的过程中,即使后面逻辑完全一致,不用负数 height 做 Key 依然会有 bug. ,用负数可以省去 Edge class 里面一个变量,最主要的是,在调用 Arrays.sort() 时,可以保证对于同一个 x 位置,在增加时永远先处理 height 最高的建筑,而删除时永远先删除 height 最小的建筑,从而保证了在同一个高度上多个建筑 Edge 重叠时,算法的正确性。
如何正确处理建筑清光之后,剩余高度为 0 的情况?一开始在 TreeMap 里加一个 (0, 1) 的 entry,代表始终有一个高度为 0 的墙,即地面;
O(Edge 排序) + O(Edge 数 * (插入 / 删除 + 查找))
O(2n log 2n) + O(2n * (log 2n + log 2n)) = O(n log n)
Last updated
Was this helpful?