【C/C++】731. 我的日程安排表 II

178 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情


题目链接:731. 我的日程安排表 II

题目描述

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。

MyCalendar 有一个 book(int start, int end) 方法。它意味着在 startend 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end

当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。

每次调用 MyCalendar.book 方法时,如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)

提示:

  • 每个测试用例,调用 MyCalendar.book 函数最多不超过 1000 次。
  • 调用函数 MyCalendar.book(start, end) 时, start 和 end 的取值范围为 [0,109][0, 10^9]

示例 1:

MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true
解释: 
前两个日程安排可以添加至日历中。 第三个日程安排会导致双重预订,但可以添加至日历中。
第四个日程安排活动(5,15)不能添加至日历中,因为它会导致三重预订。
第五个日程安排(5,10)可以添加至日历中,因为它未使用已经双重预订的时间10。
第六个日程安排(25,55)可以添加至日历中,因为时间 [25,40] 将和第三个日程安排双重预订;
时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。

整理题意

题目给定很多个时间区间 [start, end] 表示日程安排,规定每个时间区间最多可以重叠一次,也就时间区间重叠的部分不能超过两个。若给定的时间区间与之前已经预订的时间区间存在三重预订,那么就无需记录该时间区间并返回 false,否则记录该时间区间并返回 true

解题思路分析

题目预订时间区间的数量在 1000 以内,我们可以通过暴力遍历来解决:

方法一:暴力遍历

  • 记录所有已经预订的时间区间和存在两次重叠的时间区间;
  • 对于给定的时间区间,首先遍历检查是否与两次重叠的时间区间存在交集;
  • 如果存在就无法安排当前时间区间,直接返回 false
  • 如果不存在冲突,那么首先添加记录当前时间区间与已经预订的时间区间存在重叠的时间区间,再将当前时间区间添加至已经预订的时间区间中。

方法二:差分数组

  • 对于每次预订的时间区间我们还可以采用差分数组进行记录,在 start 处进行 +1 操作,在 end 处进行 -1 操作,那么差分数组的前缀和就表示在当前时间内的日程数量。
  • 那么对于当前给定的时间区间,我们可以尝试将其预订,检查预订后的差分数组前缀和是否存在区间内日程数量大于 2 的情况,如果不存在说明无冲突,可以添加,否则取消当前日程。

具体实现

方法一:暴力遍历

  • 存储不定长的区间数组使用数据结构:vector<pair<int, int>> overlaps, booked;
  • 对于当前区间 [start, end] 与所遍历到的区间 [l, r] 是否存在交集的判断为:如果 start < r && end > l 表示当前 [start, end][l, r] 存在交集。

方法二:差分数组

由于本题中 startend 数据范围较大(10910^9),我们可以利用有序的 map 进行计数即可。

复杂度分析

方法一:暴力遍历

  • 时间复杂度:O(n2)O(n^2), 其中 n 表示日程安排的数量。由于每次在进行预定时,都需要遍历所有已经预定的行程安排。
  • 空间复杂度:O(n)O(n),其中 n 表示日程安排的数量。需要保存所有已经预定的行程。

方法二:差分数组

  • 时间复杂度:O(n2logn)O(n^2 * \log n), 其中 n 表示日程安排的数量。每次求的最大的预定需要遍历所有的日程安排,map 的操作时间为 O(logn)O(\log n)
  • 空间复杂度:O(n)O(n),其中 n 表示日程安排的数量。需要空间存储所有的日程安排计数,需要的空间为 O(n)O(n)

代码实现

方法一:暴力遍历

class MyCalendarTwo {
private:
    //overlaps记录重叠部分,booked记录已经预订的区间
    vector<pair<int, int>> overlaps, booked;
public:
    MyCalendarTwo() {
        overlaps.clear();
        booked.clear();
    }
    
    bool book(int start, int end) {
        //判断是否与重叠区间有交集
        for(auto &p : overlaps){
            int l = p.first;
            int r = p.second;
            //如果于重叠区间有交集就不能再预订
            if(start < r && end > l) return false;
        }
        //添加重叠区间
        for(auto &p : booked){
            int l = p.first;
            int r = p.second;
            //如果有交集
            if(start < r && end > l){
                overlaps.push_back(make_pair(max(start, l), min(end, r)));
            }
        }
        booked.push_back(make_pair(start, end));
        return true;
    }
};

/**
 * Your MyCalendarTwo object will be instantiated and called as such:
 * MyCalendarTwo* obj = new MyCalendarTwo();
 * bool param_1 = obj->book(start,end);
 */

方法二:差分数组

class MyCalendarTwo {
private:
    //由于数值范围较大,采用 mp 作为差分数组
    map<int, int> mp;
public:
    MyCalendarTwo() {
        //清空差分数组
        mp.clear();
    }
    
    bool book(int start, int end) {
        //pre 记录差分前缀和
        int pre = 0;
        //尝试将当前 [start, end] 区间预定
        mp[start]++;
        mp[end]--;
        //检查差分数组是否存在三重预定
        for(auto &p : mp){
            pre += p.second;
            //当前区间内的预订数量超过了2个
            if(pre > 2){
                //取消当前 [start, end] 预定
                mp[start]--;
                mp[end]++;
                return false;
            }
        }
        return true;
    }
};

/**
 * Your MyCalendarTwo object will be instantiated and called as such:
 * MyCalendarTwo* obj = new MyCalendarTwo();
 * bool param_1 = obj->book(start,end);
 */

总结

  • 需要注意判断两个区间是否存在交集的方法:如果 start < r && end > l 表示当前 [start, end][l, r] 存在交集。
  • 对于存储区间的方法:采用不定长数组存储 pair 类型数据:vector<pair<int, int>> overlaps, booked;
  • 该题还有线段树的解题方法。
  • 测试结果:

731.png

731 差分数组.png 对比测试结果可知,由于差分数组的解法使用到了有序 map 的数据结构,对 map 中的元素进行操作的时间复杂度为 O(logn)O(\log n);而暴力遍历的方法中使用数组进行存储,对数组中的元素进行操作的时间复杂度为 O(1)O(1),所以暴力遍历的方法在时间复杂度上要优于差分数组的方法(空间复杂度的差距不是很大),但是对于题目本身来说,差分数组的解题思维很巧妙,是需要掌握的,暴力遍历不存在任何思维方面的技巧。

结束语

要相信,自律的人总能够找到属于自己的亮光。每天坚持锻炼、读书、复盘等习惯,时间长了,你会发现自己的谈吐和思维都发生了变化。自律带给你的不仅是容貌和气质上的改变,更是帮助你保持一种健康、积极的生活状态。新的一天,加油!