|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[729. 我的日程安排表 I]()** ,难度为 **中等**。 |
| 4 | + |
| 5 | +Tag : 「模拟」、「红黑树」、「线段树」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +实现一个 `MyCalendar` 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。 |
| 10 | + |
| 11 | +当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。 |
| 12 | + |
| 13 | +日程可以用一对整数 `start` 和 `end` 表示,这里的时间是半开区间,即 $[start, end)$, 实数 $x$ 的范围为, $start <= x < end$ 。 |
| 14 | + |
| 15 | +实现 MyCalendar 类: |
| 16 | + |
| 17 | +* `MyCalendar()` 初始化日历对象。 |
| 18 | +* `boolean book(int start, int end)` 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 `true`。否则,返回 `false` 并且不要将该日程安排添加到日历中。 |
| 19 | + |
| 20 | +示例: |
| 21 | +``` |
| 22 | +输入: |
| 23 | +["MyCalendar", "book", "book", "book"] |
| 24 | +[[], [10, 20], [15, 25], [20, 30]] |
| 25 | +
|
| 26 | +输出: |
| 27 | +[null, true, false, true] |
| 28 | +
|
| 29 | +解释: |
| 30 | +MyCalendar myCalendar = new MyCalendar(); |
| 31 | +myCalendar.book(10, 20); // return True |
| 32 | +myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。 |
| 33 | +myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。 |
| 34 | +``` |
| 35 | + |
| 36 | +提示: |
| 37 | +* $0 <= start < end <= 10^9$ |
| 38 | +* 每个测试用例,调用 `book` 方法的次数最多不超过 $1000$ 次。 |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +### 模拟 |
| 43 | + |
| 44 | +利用 `book` 操作最多调用 $1000$ 次,我们可以使用一个数组存储所有已被预定的日期 $[start, end - 1]$,对于每次 `book` 操作,检查当前传入的 $[start, end)$ 是否会与已有的日期冲突,冲突返回 `False`,否则将 $[start, end- 1]$ 插入数组并返回 `True`。 |
| 45 | + |
| 46 | +代码: |
| 47 | +```Java |
| 48 | +class MyCalendar { |
| 49 | + List<int[]> list = new ArrayList<>(); |
| 50 | + public boolean book(int start, int end) { |
| 51 | + end--; |
| 52 | + for (int[] info : list) { |
| 53 | + int l = info[0], r = info[1]; |
| 54 | + if (start > r || end < l) continue; |
| 55 | + return false; |
| 56 | + } |
| 57 | + list.add(new int[]{start, end}); |
| 58 | + return true; |
| 59 | + } |
| 60 | +} |
| 61 | +``` |
| 62 | +* 时间复杂度:令 $n$ 为 `book` 的最大调用次数,复杂度为 $O(n^2)$ |
| 63 | +* 空间复杂度:$O(n)$ |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +### 红黑树 |
| 68 | + |
| 69 | +解法一中,每次的 `book` 操作我们都不可避免的需要遍历所有已存在的日期。 |
| 70 | + |
| 71 | +实际上,如果我们使用 `TreeMap`(底层为红黑树)来维护所有日期,可以避免对所有已存在的日期进行遍历。 |
| 72 | + |
| 73 | + |
| 74 | +代码: |
| 75 | +```Java |
| 76 | +class MyCalendar { |
| 77 | + TreeMap<Integer, Integer> tm = new TreeMap<>(); |
| 78 | + public boolean book(int start, int end) { |
| 79 | + Integer prev = tm.floorKey(start), next = tm.ceilingKey(start); |
| 80 | + if ((prev == null || tm.get(prev) <= start) && (next == null || end <= next)) { |
| 81 | + tm.put(start, end); |
| 82 | + return true; |
| 83 | + } |
| 84 | + return false; |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | +* 时间复杂度:令 $n$ 为 `book` 的最大调用次数,复杂度为 $O(n * \log{n})$ |
| 89 | +* 空间复杂度:$O(n)$ |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +### 线段树(动态开点) |
| 94 | + |
| 95 | +对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 * n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。 |
| 96 | + |
| 97 | +如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 `MLE` 问题。 |
| 98 | + |
| 99 | +但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 $1e9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。 |
| 100 | + |
| 101 | +动态开点的优势在于,不需要事前构造空树,而是在插入操作 `add` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1 | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。 |
| 102 | + |
| 103 | +由于存在「懒标记」,线段树的插入和查询都是 $\log{n}$ 的,因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 * n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。 |
| 104 | + |
| 105 | +动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 * n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 * m * \log{n}$,其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。 |
| 106 | + |
| 107 | +当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 * 10^6$ 以上)。 |
| 108 | + |
| 109 | +代码: |
| 110 | +```Java |
| 111 | +class MyCalendar { |
| 112 | + class Node { |
| 113 | + // ls 和 rs 分别代表当前节点的左右子节点在 tr 的下标 |
| 114 | + // val 代表当前节点有多少数 |
| 115 | + // add 为懒标记 |
| 116 | + int ls, rs, add, val; |
| 117 | + } |
| 118 | + int N = (int)1e9, M = 120010, cnt = 1; |
| 119 | + Node[] tr = new Node[M]; |
| 120 | + void update(int u, int lc, int rc, int l, int r, int v) { |
| 121 | + if (l <= lc && rc <= r) { |
| 122 | + tr[u].val += (rc - lc + 1) * v; |
| 123 | + tr[u].add += v; |
| 124 | + return ; |
| 125 | + } |
| 126 | + lazyCreate(u); |
| 127 | + pushdown(u, rc - lc + 1); |
| 128 | + int mid = lc + rc >> 1; |
| 129 | + if (l <= mid) update(tr[u].ls, lc, mid, l, r, v); |
| 130 | + if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v); |
| 131 | + pushup(u); |
| 132 | + } |
| 133 | + int query(int u, int lc, int rc, int l, int r) { |
| 134 | + if (l <= lc && rc <= r) return tr[u].val; |
| 135 | + lazyCreate(u); |
| 136 | + pushdown(u, rc - lc + 1); |
| 137 | + int mid = lc + rc >> 1, ans = 0; |
| 138 | + if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r); |
| 139 | + if (r > mid) ans += query(tr[u].rs, mid + 1, rc, l, r); |
| 140 | + return ans; |
| 141 | + } |
| 142 | + void lazyCreate(int u) { |
| 143 | + if (tr[u] == null) tr[u] = new Node(); |
| 144 | + if (tr[u].ls == 0) { |
| 145 | + tr[u].ls = ++cnt; |
| 146 | + tr[tr[u].ls] = new Node(); |
| 147 | + } |
| 148 | + if (tr[u].rs == 0) { |
| 149 | + tr[u].rs = ++cnt; |
| 150 | + tr[tr[u].rs] = new Node(); |
| 151 | + } |
| 152 | + } |
| 153 | + void pushdown(int u, int len) { |
| 154 | + tr[tr[u].ls].add += tr[u].add; tr[tr[u].rs].add += tr[u].add; |
| 155 | + tr[tr[u].ls].val += len / 2 * tr[u].add; tr[tr[u].rs].val += (len - len / 2) * tr[u].add; |
| 156 | + tr[u].add = 0; |
| 157 | + } |
| 158 | + void pushup(int u) { |
| 159 | + tr[u].val = tr[tr[u].ls].val + tr[tr[u].rs].val; |
| 160 | + } |
| 161 | + public boolean book(int start, int end) { |
| 162 | + if (query(1, 1, N + 1, start + 1, end) > 0) return false; |
| 163 | + update(1, 1, N + 1, start + 1, end, 1); |
| 164 | + return true; |
| 165 | + } |
| 166 | +} |
| 167 | +``` |
| 168 | +* 时间复杂度:令 $n$ 为值域大小,本题固定为 $1e9$,线段树的查询和增加复杂度均为 $O(\log{n})$ |
| 169 | +* 空间复杂度:令询问数量为 $m$,复杂度为 $O(m\log{n})$ |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +### 最后 |
| 174 | + |
| 175 | +这是我们「刷穿 LeetCode」系列文章的第 `No.729` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 176 | + |
| 177 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 178 | + |
| 179 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 180 | + |
| 181 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 182 | + |
0 commit comments