持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第32天,点击查看活动详情
题目链接:715. Range 模块
题目大意
题目详细描述以及示例和提示可以点击 题目链接 或 715. Range 模块(一)进行查看,这里只对题目大意进行一个描述。
在 中给定区间范围,对给定区间范围做添加、删除和查询操作:
- 添加操作:将给定区间标记为可跟踪状态,也就是在查询时如果查询的是该区间内的子区间时返回
true。 - 删除操作:将给定区间标记为不可跟踪状态,也就是在查询时如果查询的是该区间内的子区间时返回
false。 - 查询操作:查询给定区间是否都为可跟踪状态,是返回
true,否则返回false。
需要注意给定区间都是 左闭右开区间,包括左区间端点但不包括右区间端点。
解题思路分析
在上一节 715. Range 模块(二) 中对解题的整体思路进行了详细描述,这里对解题思路进行大致概述。
- 采用 有序集合 进行存储区间节点;
- 在有序集合中进行 二分查找 确定即将添加和删除的区间在集合中的位置。
- 将操作区间与集合中 重叠的区间 进行合并。
- 不断维护集合中的区间,保证区间两两不能合并成一个更大的连续区间,这样一来查询操作
queryRange(left, right)就会变得非常方便,仅需在集合中查找判断即可。
具体实现
添加操作 addRange(left, right)
- 首先在有序集合中二分找到第一个大于
left的区间节点it:auto it = mp.upper_bound(left); - 排除集合中不存在小于等于
left的情况,也就是判断集合中是否存在 小于等于left的区间节点:if(it != mp.begin())。(对于不存在的情况不做处理) - 令开始节点
start为最大且包含left的区间节点,也就是前一个节点一定包含left,且是最靠近left的:auto start = prev(it);。(prev()函数表示去当前迭代器指向元素的前一个元素) - 如果被
start右区间完整包含,无需做任何处理,直接返回,因为添加的区间已经被完整跟踪。 - 如果当前区间
start与[left, right]有交集,也就是添加的右区间端点大于start的右区间端点(right > start->second),此时首先将区间左端点进行合并,然后删除当前start区间,因为start已经被包含在[left, right]中了。 - 剩余的情况为
start->second < left,与当前区间无交集,不做处理。 - 然后处理右区间端点,将集合中被右区间端点包含的区间全部删除,也就是合并区间,遍历的时候从
it开始遍历,因为在处理左区间端点时,也就是it前一个区间start已经处理了,所以可以直接从it开始遍历。
这里需要注意删除迭代器的手法,删除节点
it之后函数erase(it)会返回it后面一个节点的迭代器。也可以写成这样mp.erase(it++);因为会先将it传递给erase()函数,再执行it++,同时erase()函数会在it++之后执行,完美删除当前迭代器所指向的节点。
- 不断维护合并区间后的右端点最大值。
- 向集合中插入当前合并后的集合
mp[left] = right;
删除操作 removeRange(left, right)
- 首先在有序集合中找到第一个大于
left的区间节点it:auto it = mp.upper_bound(left); - 排除集合中不存在小于等于
left的情况,也就是判断集合中是否存在 小于等于left的区间节点:if(it != mp.begin())。(对于不存在的情况不做处理) - 找到最大的一个小于等于
left的区间节点,也就是前面一个节点auto start = prev(it); - 如果当前区间包含被删除区间,也就是与当前区间做减集,先记录当前区间的右区间节点值
int ri = start->second;, - 如果当前区间左端点与删除区间的左端点相同,因为已经记录了
ri,所以可以直接删除当前区间了 - 如果删除区间的左区间小于该区间的左区间,更新减集后前一个区间的右端点为
start->second = left; - 如果第二块区间不为空则插入该区间,
mp[right] = ri;,因为right <= ri = start->second,那么排除相等时空的情况,剩下就是不为空的情况,也就是right < ri - 如果
start->second < right,要删除的区间大于当前区间,且与删除区间有交集 - 如果当前区间左端点与删除区间的左端点相同,可以直接删除当前区间节点
mp.erase(start); - 剩下的情况为
start->first < left,直接修改start->second即可start->second = left; - 删除
[left, right]中的区间节点
查询操作 queryRange(left, right)
- 先在有序集合中查找第一个大于
left的区间节点 - 如果集合中存在 小于等于
left的区间节点 - 取前面一个小于等于
left的区间节点 - 因为
start->first一定小于等于left,也就是一定包含left,所以只需判断start->second是否包含right即可 - 因为添加区间的操作保证集合中节点一定严格递增,不会存在相等情况,所以只要
right大于start->second就一定存在至少start->second不在集合区间内,也不存在下一个区间节点的left与当前区间的right重合的情况,这种情况会在添加区间时合并
复杂度分析
- 时间复杂度:对于操作
queryRange,时间复杂度为 ,其中a是操作addRange的次数,r是操作removeRange的次数。对于操作addRange和removeRange,时间复杂度为均摊 ,这是因为addRange操作最多添加一个区间,removeRange最多添加两个区间,每一个添加的区间只会在未来的操作中被移除一次,因此均摊时间复杂度为对有序集合 / 有序映射常数次操作需要的时间,即为 。 - 空间复杂度:,即为有序集合 / 有序映射需要使用的空间
代码实现
添加操作 addRange(left, right)
void addRange(int left, int right) {
//二分找到有序集合中第一个大于 left 的区间节点 it
auto it = mp.upper_bound(left);
//集合中存在小于等于 left 的区间节点
if(it != mp.begin()){
//令开始节点为最大包含 left 的区间节点
auto start = prev(it);
//如果被右区间完整包含,无需做任何处理,直接返回
if(right <= start->second){
return ;
}
//当前区间 start 与 [left, right] 有交集
if(start->second >= left){
//将区间左端点合并,取并集
left = start->first;
mp.erase(start);
}
//剩余的情况为 start->second < left 无交集情况可不管
}
//处理区间右端点(合并区间)
while(it != mp.end() && it->first <= right){
//不断维护合并区间后的右区间端点值为最大
right = max(right, it->second);
//注意删除迭代器的手法,也可以写成这样 mp.erase(it++);
it = mp.erase(it);
}
//向集合中插入当前合并后的集合
mp[left] = right;
}
删除操作 removeRange(left, right)
void removeRange(int left, int right) {
//先在集合中找到第一个大于 left 的区间节点
auto it = mp.upper_bound(left);
//存在小于等于 left 的节点
if(it != mp.begin()){
//找到最大的一个小于等于 left 的区间节点,也就是前面一个节点
auto start = prev(it);
//判断当前区间是否包含被删除区间
if(start->second >= right){
//记录当前区间的右区间节点值
int ri = start->second;
//如果当前区间左端点与删除区间的左端点相同
if(start->first == left){
mp.erase(start);
}
//如果删除区间的左区间小于该区间的左区间
else{
//更新删除[left, right] 后的第一个区间
start->second = left;
}
//如果第二块区间不为空则插入该区间
if(right != ri){
mp[right] = ri;
}
//区间已经删除完成,可以直接返回
return ;
}
//与删除区间有交集
else if(start->second > left){
//如果当前区间左端点与删除区间的左端点相同
if(start->first == left){
mp.erase(start);
}
//剩下的情况为 start->first < left
else{
start->second = left;
}
}
}
//删除 [left, right] 中的区间节点
while(it != mp.end() && it->first < right){
//如果当前 it 节点区间完全在被删区间中
if(it->second <= right){
//删除 it 节点区间
it = mp.erase(it);
}
//不完全在区间中,it->first < right && it->second > right
else{
//更新删除后的剩余区间
mp[right] = it->second;
mp.erase(it);
//这里需要注意不能修改 it->first 的值来更新区间
// it->first = right; 这样写是错误的
break;
}
}
}
查询操作 queryRange(left, right)
bool queryRange(int left, int right) {
//先在有序集合中查找第一个大于left的区间节点
auto it = mp.upper_bound(left);
//如果集合中存在 小于等于 left 的区间节点
if(it != mp.begin()){
//取前面一个小于等于 left 的区间节点
auto start = prev(it);
//因为 start->first 一定小于等于 left,也就是一定包含 left
//所以只需判断 start->second 是否包含 right 即可
if(start->second >= right) return true;
//因为添加区间的操作保证集合中节点一定严格递增,不会存在相等情况
//所以只要right 大于 start->second 就一定存在至少 start->second 不在集合区间内
//也不存在 下一个区间节点的 left 与 当前区间的 right 重合的情况,这种情况会在添加区间时合并
else return false;
}
//it == mp.begin() 说明left不被集合中区间所包含
else return false;
}
完整代码(上面已实现部分用省略号代替)
class RangeModule {
private:
//创建有序集合存储区间[l, r]: mp[l] = r;
map<int, int> mp;
//也可以使用set<pair<int, int>> 进行存储,不过map更为简洁
public:
RangeModule() {
//清空有序集合
mp.clear();
}
//添加区间[left, right]
void addRange(int left, int right) {
……
}
//查询区间[left, right]是否完整在集合中存在
bool queryRange(int left, int right) {
……
}
//删除区间[left, right]
void removeRange(int left, int right) {
……
}
};
/**
* Your RangeModule object will be instantiated and called as such:
* RangeModule* obj = new RangeModule();
* obj->addRange(left,right);
* bool param_2 = obj->queryRange(left,right);
* obj->removeRange(left,right);
*/
总结
- 在集合中对迭代器所指向的元素进行删除操作时(
mp.erase(iter))需要注意删除后迭代器指向哪,如果继续使用迭代器会导致程序行为不可知,究其原因是map是关联容器,对于关联容器来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用;否则会导致程序无定义的行为。 - 该题需要注意的细节较多,分类讨论的情况也比较多,但了解其核心思想后,其实在思维上面并不是很难,重点在于 有序集合 的使用上,以及代码实现部分。
- 需要注意
map映射集合中的第一个元素key是不能直接修改的,如果想要修改key,需要先删除当前映射,再添加新的映射来实现。 - 测试结果:
结束语
我们很难知道前路有多长,也不一定清楚自己究竟要奔跑多久,但迈开大步向前进、拼尽全力去生活,就是给人生最好的答卷。努力不为感动谁,只为不与最好的自己失之交臂。趁着最美好的年华,继续奋斗吧。