【C/C++】715. Range 模块(一)

319 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情


题目链接: 715. Range 模块

题目描述

Range 模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。

半开区间 [left, right) 表示所有 leftx<rightleft \leqslant x < right 的实数 xx

实现 RangeModule 类:

RangeModule() 初始化数据结构的对象。 void addRange(int left, int right) 添加 半开区间 [left, right),跟踪该区间中的每个实数。添加与当前跟踪的数字部分重叠的区间时,应当添加在区间 [left, right) 中尚未跟踪的任何数字到该区间中。 boolean queryRange(int left, int right) 只有在当前正在跟踪区间 [left, right) 中的每一个实数时,才返回 true ,否则返回 falsevoid removeRange(int left, int right) 停止跟踪 半开区间 [left, right) 中当前正在跟踪的每个实数。

提示:

  • 1left<right1091 \leqslant left < right \leqslant 10^9
  • 在单个测试用例中,对 addRange 、  queryRange 和 removeRange 的调用总数不超过 10410^4 次

示例 1:

输入
["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"]
[[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]
输出
[null, null, null, true, false, true]

解释
RangeModule rangeModule = new RangeModule();
rangeModule.addRange(10, 20);
rangeModule.removeRange(14, 16);
rangeModule.queryRange(10, 14); 返回 true (区间 [10, 14) 中的每个数都正在被跟踪)rangeModule.queryRange(13, 15); 返回 false(未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字)rangeModule.queryRange(16, 17); 返回 true (尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪)

整理题意

[1,109][1, 10^9] 中给定区间范围,对给定区间范围做添加、删除和查询操作:

  • 添加操作:将给定区间标记为可跟踪状态,也就是在查询时如果查询的是该区间内的子区间时返回 true
  • 删除操作:将给定区间标记为不可跟踪状态,也就是在查询时如果查询的是该区间内的子区间时返回 false
  • 查询操作:查询给定区间是否都为可跟踪状态,是返回 true,否则返回 false

需要注意给定区间都是 左闭右开区间,包括左区间端点但不包括右区间端点。

解题思路分析

观察题目数据范围在 [1,109][1, 10^9] 中给定区间范围,数据范围较大,无法使用类似数组的连续存储空间进行存储,考虑使用哈希表。(在使用哈希表时,根据使用需求来决定使用有序 map 还是无序的 unordered_map)由于此处我们要对区间进行插入、删除和查询操作,难免需要遍历所有区间节点,那么选择 有序map 集合来存储区间节点更为合适,这样在选择插入位置、删除位置以及查询位置时可以使用 二分查找 来降低时间复杂度。

该题所需的数据结构重点为有序上,利用有序性,可以使用二分查找来降低时间复杂度,那么同样为有序集合的 set,也是可以选择的数据存储结构。

  • 使用 set:声明为 set<pair<int, int>>,分别对应 set.first = left, set.second = right
  • 使用 map:声明为 map<int, int>,分别对应 map[left] = [right] 由于使用 map 更为简洁,所以使用 map 进行存储。

解题方法:有序集合 map

对于有序集合中的区间,我们需要保证区间两两不能合并成一个更大的连续区间,也就是在添加和删除区间时我们需要将重叠或连续区间合并成为一个更大的连续区间。

这样一来查询操作 queryRange 就会变得非常方便:对于 queryRange(left, right) 来说,我们只需要判断集合中是否存在一个区间完全包含 [left, right] 即可。

对于 添加删除 操作,都需要找到给定区间在有序集合中的位置,那么如何准确的定位给定区间的具体位置呢?

  • 如果我们二分 第一个大于等于 给定区间左端点的区间位置(lower_bound(left)),此时二分结果有三种可能:
    1. 二分得到的左区间端点 等于 给定区间的左端点;
    2. 二分得到的左区间端点 大于 给定区间的左端点;
    3. 不存在 大于等于给定区间左端点的区间;
  • 如果我们二分 第一个大于 给定区间左端点的区间位置(upper_bound(left)),此时二分结果有两种可能:
    1. 二分得到的左区间端点 大于 给定区间的左端点;
    2. 不存在 大于等于给定区间左端点的区间; 显然结果可能性更少的 upper_bound(left) 更为合适,这样能减少判断条件来确定给定区间所在位置。

我们二分得到的第一个大于给定区间左端点的区间位置 iter 表示迭代器指向的位置,由于该区间节点不包括 left,所以我们取该节点的前一个区间节点 prev(iter)。需要注意这里要特判一下 iter 迭代器是否指向区间集合的首部,也就是 iter 是否等于 mp.begin()

  • 如果 iter == mp.begin(),表示left 为集合中最小的左区间节点;
  • 如果 iter != mp.begin(),那么我们可以取该节点的前一个区间节点 prev(iter)。 那么区间节点 prev(iter) 的左端点值一定是小于等于 left 的,换句话说也就是一定在 left 的左边,区间左端点一定是小于 left 的。如果用 lower_bound(left) 的话还需要特判相等的情况下就不用取 prev(iter),所以直接用 upper_bound(left) 可以省去特判相等的操作。

注意这里的 prev(iter) 操作,返回的是 iter 上一个迭代器,不改变 iter。这里的 prev(iter) 也就是集合中最后一个小于等于 left 的区间节点。

阶段总结

  • 看到题目中有插入和删除操作,且需要确定插入位置,可以想到使用有序集合来优化插入和删除操作,这样在选择插入位置、删除位置以及查询所需位置时可以使用 二分查找 来降低时间复杂度。
  • 在有序集合中二分查找时通常使用 upper_bound(left)prev(iter) 搭配使用,来确定位置更为方便。 以上对该题进行了题意整理,解题思路进行了分析,包括解题方法和如何使用该方法解题进行了大致描述,更具体的解题步骤分析在下一节中会进行详细叙述。

结束语

无论什么年纪,只有不断提升自己,精神世界才会充实丰盛,眼界才会愈加开阔,也才能更有底气、更加从容地面对人生的荆棘坎坷。你是什么样子,完全取决于你自己。保持学习、不断提升,是给生活最好的回馈。