本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。
为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。
由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。
实现一个 MyCalendar
类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。
日程可以用一对整数 start
和 end
表示,这里的时间是半开区间,即 [start, end)
, 实数 x
的范围为, start <= x < end
。
实现 MyCalendar
类:
MyCalendar()
初始化日历对象。boolean book(int start, int end)
如果可以将日程安排成功添加到日历中而不会导致重复预订,返回true
。否则,返回false
并且不要将该日程安排添加到日历中。
示例:
js
输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
输出:
[null, true, false, true]
解释:
js
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。
提示:
0 <= start < end <= 10^9
- 每个测试用例,调用
book
方法的次数最多不超过1000
次。
这道题来自:Weekly Contest 59,类似处理区间模型的题有:
解法1 直接遍历
我们记录下所有已经预订的课程安排区间,当我们预订新的区间 [ start , end ) [\textit{start}, \textit{end}) [start,end) 时,此时检查当前已经预订的每个日程安排是否与新日程安排冲突。若不冲突,则可以添加新的日程安排。
- 对于两个区间 [ s 1 , e 1 ) [s_1, e_1) [s1,e1) 和 [ s 2 , e 2 ) [s_2, e_2) [s2,e2) ,如果二者没有交集,则此时应当满足 s 1 ≥ e 2 s_1 \ge e_2 s1≥e2 或者 s 2 ≥ e 1 s_2 \ge e_1 s2≥e1 ,这就意味着如果满足 s 1 < e 2 s_1 < e_2 s1<e2 并且 s 2 < e 1 s_2 < e_1 s2<e1 。
cpp
class MyCalendar {
private:
vector<pair<int, int>> booked;
public:
bool book(int start, int end) {
for (auto &[l, r] : booked)
if (l < end && start < r) return false;
booked.emplace_back(start, end);
return true;
}
};
复杂度分析:
- 时间复杂度: O ( n 2 ) O(n^2) O(n2) ,其中 n n n 表示日程安排的数量。由于每次在进行预订时,都需要遍历所有已经预订的行程安排。
- 空间复杂度: O ( n ) O(n) O(n) ,其中 n n n 表示日程安排的数量。需要保存所有已经预订的行程。
解法2 二分查找
如果我们按时间顺序维护日程安排,则可以通过二分查找日程安排的情况来检查新日程安排是否可以预订,若可以预订则在排序结构中更新插入日程安排。
需要一个数据结构能够保持元素排序和支持快速插入 ,可以用 TreeSet \texttt{TreeSet} TreeSet 来构建。对于给定的区间 [ s t a r t , e n d ) [start,end) [start,end) ,我们每次查找起点大于等于 end \textit{end} end 的第一个区间 [ l 1 , r 1 ) [l_1,r_1) [l1,r1) ,同时紧挨着 [ l 1 , r 1 ) [l_1,r_1) [l1,r1) 的前一个区间为 [ l 2 , r 2 ) [l_2,r_2) [l2,r2) ,此时如果满足 r 2 ≤ start < end ≤ l 1 r_2 \le \textit{start} < \textit{end} \le l_1 r2≤start<end≤l1 ,则该区间可以预订。
cpp
class MyCalendar {
set<pair<int, int>> booked;
public:
bool book(int start, int end) {
auto it = booked.lower_bound({end, 0});
if (it == booked.begin() || (--it)->second <= start) {
booked.emplace(start, end);
return true;
}
return false;
}
};
复杂度分析:
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn) ,其中 n n n 表示日程安排的数量。由于每次在进行预订时,都需要进行二分查找,需要的时间为 O ( log n ) O(\log n) O(logn) 。
- 空间复杂度: O ( n ) O(n) O(n) ,其中 n n n 表示日程安排的数量。需要保存所有已经预订的行程。
解法3 线段树
利用线段树,假设我们开辟了数组 arr [ 0 , ⋯ , 1 0 9 ] \textit{arr}[0,\cdots, 10^9] arr[0,⋯,109] ,初始时每个元素的值都为 0 0 0 ,对于每次行程预订的区间 [ s t a r t , e n d ) [start, end) [start,end) ,则我们将区间中的元素 arr [ start , ⋯ , end − 1 ] \textit{arr}[\textit{start},\cdots,\textit{end}-1] arr[start,⋯,end−1] 中的每个元素都标记为 1 1 1 ,每次调用 book \texttt{book} book 时,我们只需要检测 arr [ start , ⋯ , end − 1 ] \textit{arr}[\textit{start},\cdots,\textit{end}-1] arr[start,⋯,end−1] 区间内是否有元素被标记为 1 1 1 。实际我们不必实际开辟数组 arr \textit{arr} arr ,可采用动态线段树,懒标记 lazy \textit{lazy} lazy 标记区间 [ l , r ] [l,r] [l,r] 已经被预订, tree \textit{tree} tree 记录区间 [ l , r ] [l,r] [l,r] 的是否存在标记为 1 1 1 的元素。
每次进行 book \texttt{book} book 操作时,首先判断区间 [ start , ⋯ , end − 1 ] [\textit{start},\cdots,\textit{end}-1] [start,⋯,end−1] 是否存在元素被标记,如果存在被标记为 1 1 1 的元素,则表明该区间不可预订;否则,则将可以预订。预订完成后,将 arr [ start , ⋯ , end − 1 ] \textit{arr}[\textit{start},\cdots,\textit{end}-1] arr[start,⋯,end−1] 进行标记为 1 1 1 ,并同时更新线段树。
cpp
class MyCalendar {
unordered_set<int> tree, lazy;
public:
bool query(int start, int end, int l, int r, int idx) {
if (r < start || end < l) {
return false;
}
/* 如果该区间已被预订,则直接返回 */
if (lazy.count(idx)) {
return true;
}
if (start <= l && r <= end) {
return tree.count(idx);
}
int mid = (l + r) >> 1;
return query(start, end, l, mid, 2 * idx) ||
query(start, end, mid + 1, r, 2 * idx + 1);
}
void update(int start, int end, int l, int r, int idx) {
if (r < start || end < l) {
return;
}
if (start <= l && r <= end) {
tree.emplace(idx);
lazy.emplace(idx);
} else {
int mid = (l + r) >> 1;
update(start, end, l, mid, 2 * idx);
update(start, end, mid + 1, r, 2 * idx + 1);
tree.emplace(idx);
if (lazy.count(2 * idx) && lazy.count(2 * idx + 1)) {
lazy.emplace(idx);
}
}
}
bool book(int start, int end) {
if (query(start, end - 1, 0, 1e9, 1)) {
return false;
}
update(start, end - 1, 0, 1e9, 1);
return true;
}
};
或者:
cpp
class MyCalendarTwo {
public:
MyCalendarTwo() {
}
void update(int s, int e, int val, int l, int r, int idx) {
if (r < s || l > e) return;
if (s <= l && r <= e) {
tree[idx].first += val;
tree[idx].second += val;
} else {
int mid = (l + r) >> 1;
update(s, e, val, l, mid, 2 * idx);
update(s, e, val, mid + 1, r, 2 * idx + 1);
tree[idx].first = tree[idx].second + max(tree[2 * idx].first, tree[2 * idx + 1].first);
}
}
bool book(int start, int end) {
update(start, end - 1, 1, 0, 1e9, 1);
if (tree[1].first > 2) {
update(start, end - 1, -1, 0, 1e9, 1);
return false;
}
return true;
}
private:
unordered_map<int, pair<int, int>> tree;
};
复杂度分析:
- 时间复杂度: O ( n log C ) O(n \log C) O(nlogC) ,其中 n n n 为日程安排的数量。由于使用了线段树查询,线段树的最大深度为 log C \log C logC ,每次最多会查询 log C \log C logC 个节点,每次求预定需时间复杂度为 O ( log C + log C ) O(\log C + \log C) O(logC+logC) ,因此时间复杂度为 O ( n log C ) O(n \log C) O(nlogC) ,在此 C C C 取固定值即为 1 0 9 10^9 109
- 空间复杂度: O ( n log C ) O(n \log C) O(nlogC) ,其中 n n n 为日程安排的数量。由于该解法采用的为动态线段树,线段树的最大深度为 log C \log C logC ,每次预定最多会在线段树上增加 log C \log C logC 个节点,因此空间复杂度为 O ( n log C ) O(n \log C) O(nlogC) ,在此 C C C 取固定值即为 1 0 9 10^9 109