第六章 - 高频考题(困难)
0715. Range 模块

题目地址(715. Range 模块)

https://leetcode-cn.com/problems/range-module/

题目描述

1
Range 模块是跟踪数字范围的模块。你的任务是以一种有效的方式设计和实现以下接口。
2
3
addRange(int left, int right) 添加半开区间 [left, right),跟踪该区间中的每个实数。添加与当前跟踪的数字部分重叠的区间时,应当添加在区间 [left, right) 中尚未跟踪的任何数字到该区间中。
4
queryRange(int left, int right) 只有在当前正在跟踪区间 [left, right) 中的每一个实数时,才返回 true。
5
removeRange(int left, int right) 停止跟踪区间 [left, right) 中当前正在跟踪的每个实数。
6
7
8
示例:
9
10
addRange(10, 20): null
11
removeRange(14, 16): null
12
queryRange(10, 14): true (区间 [10, 14) 中的每个数都正在被跟踪)
13
queryRange(13, 15): false (未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字)
14
queryRange(16, 17): true (尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪)
15
16
17
提示:
18
19
半开区间 [left, right) 表示所有满足 left <= x < right 的实数。
20
对 addRange, queryRange, removeRange 的所有调用中 0 < left < right < 10^9。
21
在单个测试用例中,对 addRange 的调用总数不超过 1000 次。
22
在单个测试用例中,对 queryRange 的调用总数不超过 5000 次。
23
在单个测试用例中,对 removeRange 的调用总数不超过 1000 次。
Copied!

前置知识

公司

  • 暂无

思路

直观的思路是使用端点记录已经被跟踪的区间,我们需要记录的区间信息大概是这样的:[(1,2),(3,6),(8,12)],这表示 [1,2), [3,6), [8,12) 被跟踪。
添加区间需要先查一下会不会和已有的区间和交集,如果有则融合。删除区间也是类似。关于判断是否有交集以及融合都可以采用一次遍历的方式来解决,优点是简单直接。
区间查询的话,由于被跟踪的区间是有序且不重叠的(重叠的会被我们合并),因此可是使用二分查找来加速。
官方给的解法其实就是这种。
代码:
1
class RangeModule(object):
2
def __init__(self):
3
# [(1,2),(3,6),(8,12)]
4
self.ranges = []
5
def overlap(self, left, right):
6
i, j = 0, len(self.ranges) - 1
7
while i < len(self.ranges) and self.ranges[i][1] < left:
8
i += 1
9
while j >= 0 and self.ranges[j][0] > right:
10
j -= 1
11
return i, j
12
13
def addRange(self, left, right):
14
i, j = self.overlap(left, right)
15
if i <= j:
16
left = min(left, self.ranges[i][0])
17
right = max(right, self.ranges[j][1])
18
self.ranges[i:j+1] = [(left, right)]
19
def queryRange(self, left, right):
20
i = bisect.bisect_right(self.ranges, (left, float('inf'))) - 1
21
return bool(self.ranges and self.ranges[i][0] <= left and right <= self.ranges[i][1])
22
23
def removeRange(self, left, right):
24
i, j = self.overlap(left, right)
25
merge = []
26
for k in xrange(i, j+1):
27
if self.ranges[k][0] < left:
28
merge.append((self.ranges[k][0], left))
29
if right < self.ranges[k][1]:
30
merge.append((right, self.ranges[k][1]))
31
self.ranges[i:j+1] = merge
Copied!
但其实这种做法 overlap 的时间复杂度是 $O(N)$,这部分可以优化。优化点点在于 overlap 的实现,实际上被跟踪的区间是有序的,因此这部分其实也可是二分查找。只不过我写了一半就发现不好根据结束时间查找。
参考了 这篇题解 后发现,其实我们可以将被跟踪的区块一维化处理,这样问题就简单了。比如我们不这样记录被跟踪的区间 [(1,2),(3,5),(8,12)],而是这样:[1,2,3,5,8,12]。
经过这样的处理, 数组的奇数坐标就是区间的结束点,偶数坐标就是开始点啦。这样二分就不需要像上面一样使用元组,而是使用单值了。
  • 如何查询某一个区间 [s, e] 是否被跟踪呢?我们只需要将 s, e 分别在数组中查一下。如果 s 和 e 都是同一个奇数坐标即可。
  • 插入和删除也是一样。先将 s, e 分别在数组中查一下,假设我们查到的分别为 i 和 j,接下来使用 [i, j] 更新原有区间即可。
示例1
示例2
使用不同颜色区分不同的区间,当我们要查 [3,9] 的时候。实线圈表示我们查到的索引,黑色的框框表示我们需要更新的区间。
区间更新逻辑如下:
区间更新逻辑

关键点解析

  • 二分查找的灵活使用(最左插入和最右插入)
  • 将区间一维化处理

代码

为了明白 Python 代码的含义,你需要明白 bisect_left 和 bisect_right,关于这两点我在二分查找专题讲地很清楚了,大家可以看一下。实际上这两者的区别只在于目标数组有目标值的情况,因此如果你搞不懂,可以尝试代入这种特殊情况理解。
代码支持:Python3
Python3 Code:
1
class RangeModule(object):
2
def __init__(self):
3
# [1,2,3,5,8,12]
4
self.ranges = []
5
6
def overlap(self, left, right, is_odd):
7
i = bisect_left(self.ranges, left)
8
j = bisect_right(self.ranges, right)
9
merge = []
10
if i & 1 == int(is_odd):
11
merge.append(left)
12
if j & 1 == int(is_odd):
13
merge.append(right)
14
# 修改 ranges 的 [i:j-1] 部分
15
self.ranges[i:j] = merge
16
17
def addRange(self, left, right):
18
# [1,2,3,5,8,12], 代入 left = 3, right = 5,此时需要保持不变, 就不难知道应该用 bisect_left 还是 bisect_right
19
return self.overlap(left, right, False)
20
21
def removeRange(self, left, right):
22
# [1,2,3,5,8,12], 代入 left = 3, right = 5,此时需要为 [1,2,8,12], 就不难知道应该用 bisect_left 还是 bisect_right
23
return self.overlap(left, right, True)
24
25
def queryRange(self, left, right):
26
# [1,2,3,5,8,12], 代入 left = 3, right = 5,此时需要返回 true, 就不难知道应该用 bisect_left 还是 bisect_right
27
i = bisect_right(self.ranges, left)
28
j = bisect_left(self.ranges, right)
29
return i & 1 == 1 and i == j # 都在一个区间内
Copied!
addRange 和 removeRange 中使用 bisect_left 找到左端点 l,使用 bisect_right 找到右端点,这样将 [left, right) 更新到区间 [l, r - 1] 即可。
复杂度分析
  • 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
  • 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 37K star 啦。
关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。