11### 题目描述
22
3- 这是 LeetCode 上的 ** [ 731. 我的日程安排表 II] ( https://leetcode-cn.com /problems/my-calendar-ii/solution/by-ac_oier-okkc / ) ** ,难度为 ** 中等** 。
3+ 这是 LeetCode 上的 ** [ 731. 我的日程安排表 II] ( https://leetcode.cn /problems/my-calendar-ii/solution/by-ac_oier-a1b3 / ) ** ,难度为 ** 中等** 。
44
55Tag : 「线段树(动态开点)」、「线段树」
66
@@ -40,7 +40,7 @@ MyCalendar.book(25, 55); // returns true
4040
4141---
4242
43- ### 线段树(动态开点)
43+ ### 线段树(动态开点)- 估点
4444
4545和 [ 729. 我的日程安排表 I] ( https://leetcode-cn.com/problems/my-calendar-i/solution/by-ac_oier-1znx/ ) 几乎完全一致,只需要将对「线段树」所维护的节点信息进行调整即可。
4646
@@ -56,24 +56,21 @@ MyCalendar.book(25, 55); // returns true
5656
5757但对于本题而言,由于「强制在线」的原因,我们无法进行「离散化」,同时值域大小达到 1ドルe9$ 级别,因此如果我们想要使用「线段树」进行求解,只能采取「动态开点」的方式进行。
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 \times n),ドル而开点数的预估需不能仅仅根据 $\log{n}$ 来进行,还要对常熟进行分析 ,才能得到准确的点数上界。
6262
63- 动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 4ドル * n$ 的空间,因此盲猜 $\log{n}$ 的常数在 4ドル$ 左右,保守一点可以直接估算到 6ドル,ドル因此我们可以估算点数为 6ドル * m * \log{n},ドル其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。
63+ 动态开点相比于原始的线段树实现,本质仍是使用「满二叉树」的形式进行存储,只不过是按需创建区间,如果我们是按照连续段进行查询或插入,最坏情况下仍然会占到 4ドル \times n$ 的空间,因此盲猜 $\log{n}$ 的常数在 4ドル$ 左右,保守一点可以直接估算到 6ドル,ドル因此我们可以估算点数为 6ドル \times m \times \log{n},ドル其中 $n = 1e9$ 和 $m = 1e3$ 分别代表值域大小和查询次数。
6464
6565当然一个比较实用的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开( ` Java ` 的 128ドルM$ 可以开到 5ドル \times 10^6$ 以上)。
6666
67- 代码:
67+ Java 代码:
6868``` Java
6969class MyCalendarTwo {
7070 class Node {
71- // ls 和 rs 分别代表当前节点的左右子节点在 tr 的下标
72- // add 为懒标记
73- // max 为当前区间的最大值
7471 int ls, rs, add, max;
7572 }
76- int N = (int )1e9 , M = 1000010 , cnt = 1 ;
73+ int N = (int )1e9 , M = 120010 , cnt = 1 ;
7774 Node [] tr = new Node [M ];
7875 void update (int u , int lc , int rc , int l , int r , int v ) {
7976 if (l <= lc && rc <= r) {
@@ -123,6 +120,162 @@ class MyCalendarTwo {
123120 }
124121}
125122```
123+ TypeScript 代码:
124+ ``` TypeScript
125+ class TNode {
126+ ls: number = 0 ; rs: number = 0
127+ max: number = 0 ; add: number = 0 ;
128+ }
129+ class MyCalendarTwo {
130+ N = 1e9 ; M = 120010 ; cnt = 1
131+ tr: TNode [] = new Array <TNode >(this .M )
132+ query(u : number , lc : number , rc : number , l : number , r : number ): number {
133+ if (l <= lc && rc <= r ) return this .tr [u ].max ;
134+ this .pushdown (u )
135+ let mid = lc + rc >> 1 , ans = 0
136+ if (l <= mid ) ans = Math .max (ans , this .query (this .tr [u ].ls , lc , mid , l , r ))
137+ if (r > mid ) ans = Math .max (ans , this .query (this .tr [u ].rs , mid + 1 , rc , l , r ))
138+ return ans
139+ }
140+ update(u : number , lc : number , rc : number , l : number , r : number , v : number ): number {
141+ if (l <= lc && rc <= r ) {
142+ this .tr [u ].add += v
143+ this .tr [u ].max += v
144+ return
145+ }
146+ this .pushdown (u )
147+ let mid = lc + rc >> 1
148+ if (l <= mid ) this .update (this .tr [u ].ls , lc , mid , l , r , v )
149+ if (r > mid ) this .update (this .tr [u ].rs , mid + 1 , rc , l , r , v )
150+ this .pushdup (u )
151+ }
152+ pushdown(u : number ): void {
153+ if (this .tr [u ] == null ) this .tr [u ] = new TNode ()
154+ if (this .tr [u ].ls == 0 ) {
155+ this .tr [u ].ls = ++ this .cnt
156+ this .tr [this .tr [u ].ls ] = new TNode ()
157+ }
158+ if (this .tr [u ].rs == 0 ) {
159+ this .tr [u ].rs = ++ this .cnt
160+ this .tr [this .tr [u ].rs ] = new TNode ()
161+ }
162+ const add = this .tr [u ].add
163+ this .tr [this .tr [u ].ls ].add += add ; this .tr [this .tr [u ].rs ].add += add
164+ this .tr [this .tr [u ].ls ].max += add ; this .tr [this .tr [u ].rs ].max += add
165+ this .tr [u ].add = 0
166+ }
167+ pushdup(u : number ): void {
168+ this .tr [u ].max = Math .max (this .tr [this .tr [u ].ls ].max , this .tr [this .tr [u ].rs ].max )
169+ }
170+ book(start : number , end : number ): boolean {
171+ if (this .query (1 , 1 , this .N + 1 , start + 1 , end ) >= 2 ) return false
172+ this .update (1 , 1 , this .N + 1 , start + 1 , end , 1 )
173+ return true
174+ }
175+ }
176+ ```
177+ * 时间复杂度:令 $n$ 为值域大小,本题固定为 1ドルe9,ドル线段树的查询和增加复杂度均为 $O(\log{n})$
178+ * 空间复杂度:令询问数量为 $m,ドル复杂度为 $O(m\log{n})$
179+ 180+ ---
181+ 182+ ### 线段树(动态开点)- 动态指针
183+ 184+ 利用「动态指针」实现的「动态开点」可以有效避免数组估点问题,更重要的是可以有效避免 ` new ` 大数组的初始化开销,对于 LC 这种还跟你算所有样例总时长的 OJ 来说,在不考虑 ` static ` 优化/全局数组优化 的情况下,动态指针的方式要比估点的方式来得好。
185+ 186+ Java 代码:
187+ ``` Java
188+ class MyCalendarTwo {
189+ class Node {
190+ Node ls, rs;
191+ int max, add;
192+ }
193+ int N = (int )1e9 ;
194+ Node root = new Node ();
195+ void update (Node node , int lc , int rc , int l , int r , int v ) {
196+ if (l <= lc && rc <= r) {
197+ node. add += v;
198+ node. max += v;
199+ return ;
200+ }
201+ pushdown(node);
202+ int mid = lc + rc >> 1 ;
203+ if (l <= mid) update(node. ls, lc, mid, l, r, v);
204+ if (r > mid) update(node. rs, mid + 1 , rc, l, r, v);
205+ pushup(node);
206+ }
207+ int query (Node node , int lc , int rc , int l , int r ) {
208+ if (l <= lc && rc <= r) return node. max;
209+ pushdown(node);
210+ int mid = lc + rc >> 1 , ans = 0 ;
211+ if (l <= mid) ans = query(node. ls, lc, mid, l, r);
212+ if (r > mid) ans = Math . max(ans, query(node. rs, mid + 1 , rc, l, r));
213+ return ans;
214+ }
215+ void pushdown (Node node ) {
216+ if (node. ls == null ) node. ls = new Node ();
217+ if (node. rs == null ) node. rs = new Node ();
218+ int add = node. add;
219+ node. ls. max += add; node. rs. max += add;
220+ node. ls. add += add; node. rs. add += add;
221+ node. add = 0 ;
222+ }
223+ void pushup (Node node ) {
224+ node. max = Math . max(node. ls. max, node. rs. max);
225+ }
226+ public boolean book (int start , int end ) {
227+ if (query(root, 0 , N , start, end - 1 ) >= 2 ) return false ;
228+ update(root, 0 , N , start, end - 1 , 1 );
229+ return true ;
230+ }
231+ }
232+ ```
233+ TypeScript 代码:
234+ ``` TypeScript
235+ class TNode {
236+ ls: TNode = null ; rs: TNode = null
237+ max: number = 0 ; add: number = 0
238+ }
239+ class MyCalendarTwo {
240+ root: TNode = new TNode ()
241+ update(node : TNode , lc : number , rc : number , l : number , r : number , v : number ): void {
242+ if (l <= lc && rc <= r ) {
243+ node .add += v
244+ node .max += v
245+ return
246+ }
247+ this .pushdown (node )
248+ let mid = lc + rc >> 1
249+ if (l <= mid ) this .update (node .ls , lc , mid , l , r , v )
250+ if (r > mid ) this .update (node .rs , mid + 1 , rc , l , r , v )
251+ this .pushup (node )
252+ }
253+ query(node : TNode , lc : number , rc : number , l : number , r : number ): number {
254+ if (l <= lc && rc <= r ) return node .max
255+ let mid = lc + rc >> 1 , ans = 0
256+ this .pushdown (node )
257+ if (l <= mid ) ans = this .query (node .ls , lc , mid , l , r )
258+ if (r > mid ) ans = Math .max (ans , this .query (node .rs , mid + 1 , rc , l , r ))
259+ return ans
260+ }
261+ pushdown(node : TNode ): void {
262+ if (node .ls == null ) node .ls = new TNode ()
263+ if (node .rs == null ) node .rs = new TNode ()
264+ const add = node .add
265+ node .ls .add += add ; node .rs .add += add
266+ node .ls .max += add ; node .rs .max += add
267+ node .add = 0
268+ }
269+ pushup(node : TNode ): void {
270+ node .max = Math .max (node .ls .max , node .rs .max )
271+ }
272+ book(start : number , end : number ): boolean {
273+ if (this .query (this .root , 0 , 1e9 , start , end - 1 ) >= 2 ) return false
274+ this .update (this .root , 0 , 1e9 , start , end - 1 , 1 )
275+ return true
276+ }
277+ }
278+ ```
126279* 时间复杂度:令 $n$ 为值域大小,本题固定为 1ドルe9,ドル线段树的查询和增加复杂度均为 $O(\log{n})$
127280* 空间复杂度:令询问数量为 $m,ドル复杂度为 $O(m\log{n})$
128281
0 commit comments