|
2 | 2 |
|
3 | 3 | 这是 LeetCode 上的 **[732. 我的日程安排表 III](https://leetcode-cn.com/problems/my-calendar-iii/solution/by-ac_oier-cv31/)** ,难度为 **困难**。
|
4 | 4 |
|
5 |
| -Tag : 「线段树(动态开点)」 |
| 5 | +Tag : 「线段树(动态开点)」、「分块」 |
6 | 6 |
|
7 | 7 |
|
8 | 8 |
|
@@ -42,27 +42,27 @@ myCalendarThree.book(25, 55); // 返回 3
|
42 | 42 |
|
43 | 43 | ### 线段树(动态开点)
|
44 | 44 |
|
45 |
| -和 [731. 我的日程安排表 II](https://leetcode-cn.com/problems/my-calendar-ii/solution/by-ac_oier-okkc/) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。 |
| 45 | +和 [731. 我的日程安排表 II](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491636&idx=1&sn=48bfdd56cdcc33ededd6f154ffd41f0a) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。 |
46 | 46 |
|
47 | 47 | 线段树维护的节点信息包括:
|
48 | 48 |
|
49 | 49 | * `ls/rs`: 分别代表当前节点的左右子节点在线段树数组 `tr` 中的下标;
|
50 | 50 | * `add`: 懒标记;
|
51 | 51 | * `max`: 为当前区间的最大值。
|
52 | 52 |
|
53 |
| -对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 \times n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。 |
| 53 | +对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 * n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。 |
54 | 54 |
|
55 | 55 | 如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 `MLE` 问题。
|
56 | 56 |
|
57 | 57 | 但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 $1e9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
|
58 | 58 |
|
59 |
| -动态开点的优势在于,不需要事前构造空树,而是在插入操作 `update` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1 | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。 |
| 59 | +动态开点的优势在于,不需要事前构造空树,而是在插入操作 `add` 和查询操作 `query` 时根据访问需要进行「开点」操作。由于我们不保证查询和插入都是连续的,因此对于父节点 $u$ 而言,我们不能通过 `u << 1` 和 `u << 1 | 1` 的固定方式进行访问,而要将节点 $tr[u]$ 的左右节点所在 `tr` 数组的下标进行存储,分别记为 `ls` 和 `rs` 属性。对于 $tr[u].ls = 0$ 和 $tr[u].rs = 0$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。 |
60 | 60 |
|
61 |
| -由于存在「懒标记」,线段树的插入和查询都是 $\log{n}$ 的,因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 \times n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常数进行分析,才能得到准确的点数上界。 |
| 61 | +由于存在「懒标记」,线段树的插入和查询都是 $\log{n}$ 的,因此我们在单次操作的时候,最多会创建数量级为 $\log{n}$ 的点,因此空间复杂度为 $O(m\log{n})$,而不是 $O(4 * n)$,而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。 |
62 | 62 |
|
63 | 63 | 动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 * n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 * m * \log{n}$,其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。
|
64 | 64 |
|
65 |
| -当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 \times 10^6$ 以上)。 |
| 65 | +当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 * 10^6$ 以上)。 |
66 | 66 |
|
67 | 67 | 代码:
|
68 | 68 | ```Java
|
@@ -90,7 +90,7 @@ class MyCalendarThree {
|
90 | 90 | lazyCreate(u);
|
91 | 91 | pushdown(u);
|
92 | 92 | int mid = lc + rc >> 1, ans = 0;
|
93 |
| - if (l <= mid) ans = Math.max(ans, query(tr[u].ls, lc, mid, l, r)); |
| 93 | + if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r); |
94 | 94 | if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r));
|
95 | 95 | return ans;
|
96 | 96 | }
|
@@ -124,6 +124,104 @@ class MyCalendarThree {
|
124 | 124 |
|
125 | 125 | ---
|
126 | 126 |
|
| 127 | +### 带懒标记的分块(TLE) |
| 128 | + |
| 129 | +实际上,还存在另外一种十分值得学习的算法:分块。 |
| 130 | + |
| 131 | +但该做法被奇怪的测评机制卡掉,是给每个样例都定了执行用时吗 🤣 ? |
| 132 | + |
| 133 | +旧题解没有这种做法,今天补充的,我们可以大概讲讲「分块」算法是如何解决涉及「区间修改」,也就是带懒标记的问题。 |
| 134 | + |
| 135 | +对于本题,我们设定两个块数组 `max` 和 `add`,其中 `max[idx]` 含义为块编号为 `idx` 的连续区间的最大重复值,而 `add[idx]` 代表块编号的懒标记,同时使用「哈希表」记录下某些具体位置的覆盖次数。 |
| 136 | + |
| 137 | +然后我们考虑如何指定块大小,设定一个合理的块大小是减少运算量的关键。 |
| 138 | + |
| 139 | +通常情况下,我们会设定块大小为 $\sqrt{n}$,从而确保块内最多不超过 $\sqrt{n}$ 个元素,同时块的总个数也不超过 $\sqrt{n}$,即无论我们是块内暴力,还是操作多个块,复杂度均为 $O(\sqrt{n})$。因此对于本题而言,设定块大小为 $\sqrt{1e9}$(经试验,调成 $1200010$ 能够通过较多样例),较为合适。 |
| 140 | + |
| 141 | +对于涉及「区间修改」的分块算法,我们需要在每次修改和查询前,先将懒标记下传到区间的每个元素中。 |
| 142 | + |
| 143 | +然后考虑如何处理「块内」和「块间」操作: |
| 144 | + |
| 145 | +* 块内操作: |
| 146 | + * 块内更新:直接操作 `map` 进行累加,同时使用更新后的 `map` 来维护相应的 `max[idx]`; |
| 147 | + * 块内查询:直接查询 `map` 即可; |
| 148 | +* 块间操作: |
| 149 | + * 块间更新:直接更新懒标记 `add[idx]` 即可,同时由于我们是对整个区间进行累加操作,因此最大值必然也会同步被累加,因此同步更新 `max[idx]`; |
| 150 | + * 块间查询:直接查询 `max[idx]` 即可。 |
| 151 | + |
| 152 | +代码: |
| 153 | +```Java |
| 154 | +class MyCalendarThree { |
| 155 | + static int N = (int)1e9 + 10, M = 1200010, SZ = N / M + 10; |
| 156 | + static int[] max = new int[M], add = new int[M]; |
| 157 | + static Map<Integer, Integer> map = new HashMap<>(); |
| 158 | + int minv = -1, maxv = -1; |
| 159 | + int getIdx(int x) { |
| 160 | + return x / SZ; |
| 161 | + } |
| 162 | + void add(int l, int r) { |
| 163 | + pushdown(l); pushdown(r); |
| 164 | + if (getIdx(l) == getIdx(r)) { |
| 165 | + for (int k = l; k <= r; k++) { |
| 166 | + map.put(k, map.getOrDefault(k, 0) + 1); |
| 167 | + max[getIdx(k)] = Math.max(max[getIdx(k)], map.get(k)); |
| 168 | + } |
| 169 | + } else { |
| 170 | + int i = l, j = r; |
| 171 | + while (getIdx(i) == getIdx(l)) { |
| 172 | + map.put(i, map.getOrDefault(i, 0) + 1); |
| 173 | + max[getIdx(i)] = Math.max(max[getIdx(i)], map.get(i)); |
| 174 | + i++; |
| 175 | + } |
| 176 | + while (getIdx(j) == getIdx(r)) { |
| 177 | + map.put(j, map.getOrDefault(j, 0) + 1); |
| 178 | + max[getIdx(j)] = Math.max(max[getIdx(j)], map.get(j)); |
| 179 | + j--; |
| 180 | + } |
| 181 | + for (int k = getIdx(i); k <= getIdx(j); k++) { |
| 182 | + max[k]++; add[k]++; |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + int query(int l, int r) { |
| 187 | + pushdown(l); pushdown(r); |
| 188 | + int ans = 0; |
| 189 | + if (getIdx(l) == getIdx(r)) { |
| 190 | + for (int k = l; k <= r; k++) ans = Math.max(ans, map.getOrDefault(k, 0)); |
| 191 | + } else { |
| 192 | + int i = l, j = r; |
| 193 | + while (getIdx(i) == getIdx(l)) ans = Math.max(ans, map.getOrDefault(i++, 0)); |
| 194 | + while (getIdx(j) == getIdx(r)) ans = Math.max(ans, map.getOrDefault(j--, 0)); |
| 195 | + for (int k = getIdx(i); k <= getIdx(j); k++) ans = Math.max(ans, max[k]); |
| 196 | + } |
| 197 | + return ans; |
| 198 | + } |
| 199 | + void pushdown(int x) { |
| 200 | + int idx = getIdx(x); |
| 201 | + if (add[idx] == 0) return ; |
| 202 | + int i = x, j = x + 1; |
| 203 | + while (getIdx(i) == idx) map.put(i, map.getOrDefault(i--, 0) + add[idx]); |
| 204 | + while (getIdx(j) == idx) map.put(j, map.getOrDefault(j++, 0) + add[idx]); |
| 205 | + add[idx] = 0; |
| 206 | + } |
| 207 | + public MyCalendarThree() { |
| 208 | + Arrays.fill(max, 0); |
| 209 | + Arrays.fill(add, 0); |
| 210 | + map.clear(); |
| 211 | + } |
| 212 | + public int book(int start, int end) { |
| 213 | + add(start + 1, end); |
| 214 | + minv = minv == -1 ? start : Math.min(minv, start); |
| 215 | + maxv = maxv == -1 ? end + 1 : Math.max(maxv, end + 1); |
| 216 | + return query(minv, maxv); |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
| 220 | +* 时间复杂度:令 $M$ 为块大小,复杂度为 $O(\sqrt{M})$ |
| 221 | +* 空间复杂度:$O(C \times \sqrt{M})$,其中 $C = 1e3$ 为最大调用次数 |
| 222 | + |
| 223 | +--- |
| 224 | + |
127 | 225 | ### 最后
|
128 | 226 |
|
129 | 227 | 这是我们「刷穿 LeetCode」系列文章的第 `No.732` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
|
|
0 commit comments