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

170 阅读9分钟

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


题目链接:715. Range 模块

题目大意

题目详细描述以及示例和提示可以点击 题目链接715. Range 模块(一)进行查看,这里只对题目大意进行一个描述。

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

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

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

解题思路分析

在上一节 715. Range 模块(二) 中对解题的整体思路进行了详细描述,这里对解题思路进行大致概述。

  • 采用 有序集合 进行存储区间节点;
  • 在有序集合中进行 二分查找 确定即将添加和删除的区间在集合中的位置。
  • 将操作区间与集合中 重叠的区间 进行合并。
  • 不断维护集合中的区间,保证区间两两不能合并成一个更大的连续区间,这样一来查询操作 queryRange(left, right) 就会变得非常方便,仅需在集合中查找判断即可。

具体实现

添加操作 addRange(left, right)

  1. 首先在有序集合中二分找到第一个大于 left 的区间节点 itauto it = mp.upper_bound(left);
  2. 排除集合中不存在小于等于 left 的情况,也就是判断集合中是否存在 小于等于 left 的区间节点:if(it != mp.begin())。(对于不存在的情况不做处理)
  3. 令开始节点 start 为最大且包含 left 的区间节点,也就是前一个节点一定包含 left,且是最靠近 left 的:auto start = prev(it);。(prev() 函数表示去当前迭代器指向元素的前一个元素)
  4. 如果被 start 右区间完整包含,无需做任何处理,直接返回,因为添加的区间已经被完整跟踪。
  5. 如果当前区间 start[left, right] 有交集,也就是添加的右区间端点大于 start 的右区间端点(right > start->second),此时首先将区间左端点进行合并,然后删除当前 start 区间,因为 start 已经被包含在 [left, right] 中了。
  6. 剩余的情况为 start->second < left ,与当前区间无交集,不做处理。
  7. 然后处理右区间端点,将集合中被右区间端点包含的区间全部删除,也就是合并区间,遍历的时候从 it 开始遍历,因为在处理左区间端点时,也就是 it 前一个区间 start 已经处理了,所以可以直接从 it 开始遍历。

这里需要注意删除迭代器的手法,删除节点 it 之后函数 erase(it) 会返回 it 后面一个节点的迭代器。也可以写成这样 mp.erase(it++); 因为会先将 it 传递给 erase() 函数,再执行 it++,同时 erase() 函数会在 it++ 之后执行,完美删除当前迭代器所指向的节点。

  1. 不断维护合并区间后的右端点最大值。
  2. 向集合中插入当前合并后的集合 mp[left] = right;

删除操作 removeRange(left, right)

  1. 首先在有序集合中找到第一个大于 left 的区间节点 itauto it = mp.upper_bound(left);
  2. 排除集合中不存在小于等于 left 的情况,也就是判断集合中是否存在 小于等于 left 的区间节点:if(it != mp.begin())。(对于不存在的情况不做处理)
  3. 找到最大的一个小于等于 left 的区间节点,也就是前面一个节点 auto start = prev(it);
  4. 如果当前区间包含被删除区间,也就是与当前区间做减集,先记录当前区间的右区间节点值 int ri = start->second;
  5. 如果当前区间左端点与删除区间的左端点相同,因为已经记录了 ri,所以可以直接删除当前区间了
  6. 如果删除区间的左区间小于该区间的左区间,更新减集后前一个区间的右端点为 start->second = left;
  7. 如果第二块区间不为空则插入该区间,mp[right] = ri;,因为 right <= ri = start->second,那么排除相等时空的情况,剩下就是不为空的情况,也就是 right < ri
  8. 如果 start->second < right,要删除的区间大于当前区间,且与删除区间有交集
  9. 如果当前区间左端点与删除区间的左端点相同,可以直接删除当前区间节点 mp.erase(start);
  10. 剩下的情况为 start->first < left,直接修改 start->second 即可 start->second = left;
  11. 删除 [left, right] 中的区间节点

查询操作 queryRange(left, right)

  1. 先在有序集合中查找第一个大于 left 的区间节点
  2. 如果集合中存在 小于等于 left 的区间节点
  3. 取前面一个小于等于 left 的区间节点
  4. 因为 start->first 一定小于等于 left,也就是一定包含 left,所以只需判断 start->second 是否包含 right 即可
  5. 因为添加区间的操作保证集合中节点一定严格递增,不会存在相等情况,所以只要 right 大于 start->second 就一定存在至少 start->second 不在集合区间内,也不存在下一个区间节点的 left 与当前区间的 right 重合的情况,这种情况会在添加区间时合并

复杂度分析

  • 时间复杂度:对于操作 queryRange,时间复杂度为 O(log(a+r))O(\log(a+r)),其中 a 是操作 addRange 的次数,r 是操作 removeRange 的次数。对于操作 addRangeremoveRange,时间复杂度为均摊 O(log(a+r))O(\log(a+r)),这是因为 addRange 操作最多添加一个区间,removeRange 最多添加两个区间,每一个添加的区间只会在未来的操作中被移除一次,因此均摊时间复杂度为对有序集合 / 有序映射常数次操作需要的时间,即为 O(log(a+r))O(\log(a+r))
  • 空间复杂度:O(a+r)O(a+r),即为有序集合 / 有序映射需要使用的空间

代码实现

添加操作 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,需要先删除当前映射,再添加新的映射来实现。
  • 测试结果:

微信截图_20220621185430.png

结束语

我们很难知道前路有多长,也不一定清楚自己究竟要奔跑多久,但迈开大步向前进、拼尽全力去生活,就是给人生最好的答卷。努力不为感动谁,只为不与最好的自己失之交臂。趁着最美好的年华,继续奋斗吧。