Skip to content

Commit 6bde722

Browse files
committed
✨feat: add 732
1 parent 2fe552c commit 6bde722

File tree

1 file changed

+105
-7
lines changed

1 file changed

+105
-7
lines changed

LeetCode/731-740/732. 我的日程安排表 III(困难).md

+105-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
这是 LeetCode 上的 **[732. 我的日程安排表 III](https://leetcode-cn.com/problems/my-calendar-iii/solution/by-ac_oier-cv31/)** ,难度为 **困难**
44

5-
Tag : 「线段树(动态开点)」
5+
Tag : 「线段树(动态开点)」、「分块」
66

77

88

@@ -42,27 +42,27 @@ myCalendarThree.book(25, 55); // 返回 3
4242

4343
### 线段树(动态开点)
4444

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) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。
4646

4747
线段树维护的节点信息包括:
4848

4949
* `ls/rs`: 分别代表当前节点的左右子节点在线段树数组 `tr` 中的下标;
5050
* `add`: 懒标记;
5151
* `max`: 为当前区间的最大值。
5252

53-
对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 \times n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。
53+
对于常规的线段树实现来说,都是一开始就调用 `build` 操作创建空树,而线段树一般以「满二叉树」的形式用数组存储,因此需要 $4 * n$ 的空间,并且这些空间在起始 `build` 空树的时候已经锁死。
5454

5555
如果一道题仅仅是「值域很大」的离线题(提前知晓所有的询问),我们还能通过「离散化」来进行处理,将值域映射到一个小空间去,从而解决 `MLE` 问题。
5656

5757
但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 $1e9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
5858

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$ 则是代表子节点尚未被创建,当需要访问到它们,而又尚未创建的时候,则将其进行创建。
6060

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}$ 来进行,还要对常熟进行分析,才能得到准确的点数上界。
6262

6363
动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 $4 * n$ 的空间,因此盲猜 $\log{n}$ 的常数在 $4$ 左右,保守一点可以直接估算到 $6$,因此我们可以估算点数为 $6 * m * \log{n}$,其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。
6464

65-
当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 \times 10^6$ 以上)。
65+
当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( `Java` 的 $128M$ 可以开到 $5 * 10^6$ 以上)。
6666

6767
代码:
6868
```Java
@@ -90,7 +90,7 @@ class MyCalendarThree {
9090
lazyCreate(u);
9191
pushdown(u);
9292
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);
9494
if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r));
9595
return ans;
9696
}
@@ -124,6 +124,104 @@ class MyCalendarThree {
124124

125125
---
126126

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+
127225
### 最后
128226

129227
这是我们「刷穿 LeetCode」系列文章的第 `No.732` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

0 commit comments

Comments
 (0)