LeetCode 732. My Calendar III
题目描述
Implement a
MyCalendarThree
class to store your events. A new event can always be added.Your class will have one method,
book(int start, int end)
. Formally, this represents a booking on the half open interval [start, end), the range of real numbersx
such thatstart <= x < end
.A K-booking happens when K events have some non-empty intersection (ie., there is some time that is common to all K events.)
For each call to the method
MyCalendar.book
, return an integer K representing the largest integer such that there exists a K-booking in the calendar.Your class will be called like this:
MyCalendarThree cal = new MyCalendarThree(); MyCalendarThree.book(start, end)
题目分析
这道题有一定的背景。 现在把和问题无关的描述去掉, 大致简化为如下的问题:
每次向数轴中插入一根线段, 每次插入之后返回当前数轴上各个线段之间的最大的重合数。
这道题是在看线段树专题的时候看到的。 本来以为需要建立线段树然后balabala…… 直到看到了Discuss中的
O(N),Map
什么的提示,就。。发现完全不需要线段树了。
但是也可以简单把线段树的思路讲一下。
线段树放在这道题上其实非常显然, 只需要简单的调用几次线段树的方法就可以得到结果。大致如下:
- 每次插入一条线段, 对应更新线段树中原先线段上的值。这里,线段的值就是该线段出现的次数。
- 用一个变量记录最大值。每次在插入线段时维护这个最大值。 插入完之后最大值即为当次结果返回
以上就是线段树的做法。这个做法的缺点如下:
1. 线段树写起来繁琐。 这个写过的自然懂。
2. 线段树的范围太大。 注意到这道题的数据范围对朴素线段树是十分不友好的: 插入次数 < 400, 而插入范围是
109
直接储存直接爆内存应该毫无疑问。 显然要做压缩。
3. 由于是动态查询, 我们不能事先知道线段的起始与终点, 因此压缩比较麻烦。。。
好吧下面开始正题。
这个题目其实也并没用到线段树的很多功能。所以尝试用直接记录起点终点来代表一个线段。
这样需要解决一点问题, 比如:
1. 若用一个数组来记录经过某个点的线段数目, 那么无法分清经过从该点的出发的还是只是经过的。
2. 若用两个数组来分别记录出发和经过,很难更新它们的值: 考虑到某个点既可能出发, 也可能是终止,还可能只是经过。。。当一个新的状态要被更新时, 我们无法从之前保存的状态完全恢复之前所有的信息。
换一种想法: 如果只是保存起始和终点的情况,而完全不管中间经过的点呢?
类似于图, 线段可以看做是一条有向边, 每次从一个点A
指向B
,相当于A的出度+1, B的入度 + 1。
这种情况下, 找到整个数轴上出度最大的点就是被覆盖次数最多的点。
下面是实现代码,十分简短:
class MyCalendarThree {
public:
MyCalendarThree() {
maximum = 0;
}
int book(int start, int end) {
int k = 0;
++m[start];
--m[end];
for (auto i = m.begin(); i != m.end(); ++i) {
k+= i->second;
if (maximum < k) maximum = k;
}
return maximum;
}
map<int, int> m;
int maximum;
};
其他细节
这里用
map
比vector
更加合适, 因为这里的线段的两个端点具有强离散性, 这也是线段树相比于用map
不如的地方。这里一个小trick是用了map自带的排序, 内部是用高效的方法做的。并且最后扫描的时候也十分方便。