|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[699. 掉落的方块](https://leetcode-cn.com/problems/degree-of-an-array/solution/shu-zu-ji-shu-ha-xi-biao-ji-shu-jie-fa-y-a0mg/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「线段树(动态开点)」、「线段树」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +在无限长的数轴(即 `x` 轴)上,我们根据给定的顺序放置对应的正方形方块。 |
| 10 | + |
| 11 | +第 i 个掉落的方块(`positions[i] = (left, side_length)`)是正方形,其中 `left` 表示该方块最左边的点位置(`positions[i][0]`),`side_length` 表示该方块的边长(`positions[i][1]`)。 |
| 12 | + |
| 13 | +每个方块的底部边缘平行于数轴(即 `x` 轴),并且从一个比目前所有的落地方块更高的高度掉落而下。在上一个方块结束掉落,并保持静止后,才开始掉落新方块。 |
| 14 | + |
| 15 | +方块的底边具有非常大的粘性,并将保持固定在它们所接触的任何长度表面上(无论是数轴还是其他方块)。邻接掉落的边不会过早地粘合在一起,因为只有底边才具有粘性。 |
| 16 | + |
| 17 | +返回一个堆叠高度列表 `ans`。每一个堆叠高度 `ans[i]` 表示在通过 `positions[0], positions[1], ..., positions[i]` 表示的方块掉落结束后,目前所有已经落稳的方块堆叠的最高高度。 |
| 18 | + |
| 19 | +示例 1: |
| 20 | +``` |
| 21 | +输入: [[1, 2], [2, 3], [6, 1]] |
| 22 | + |
| 23 | +输出: [2, 5, 5] |
| 24 | + |
| 25 | +解释: |
| 26 | + |
| 27 | +第一个方块 positions[0] = [1, 2] 掉落: |
| 28 | +_aa |
| 29 | +_aa |
| 30 | +------- |
| 31 | +方块最大高度为 2 。 |
| 32 | + |
| 33 | +第二个方块 positions[1] = [2, 3] 掉落: |
| 34 | +__aaa |
| 35 | +__aaa |
| 36 | +__aaa |
| 37 | +_aa__ |
| 38 | +_aa__ |
| 39 | +-------------- |
| 40 | +方块最大高度为5。 |
| 41 | +大的方块保持在较小的方块的顶部,不论它的重心在哪里,因为方块的底部边缘有非常大的粘性。 |
| 42 | + |
| 43 | +第三个方块 positions[1] = [6, 1] 掉落: |
| 44 | +__aaa |
| 45 | +__aaa |
| 46 | +__aaa |
| 47 | +_aa |
| 48 | +_aa___a |
| 49 | +-------------- |
| 50 | +方块最大高度为5。 |
| 51 | + |
| 52 | +因此,我们返回结果[2, 5, 5]。 |
| 53 | +``` |
| 54 | + |
| 55 | +示例 2: |
| 56 | +``` |
| 57 | +输入: [[100, 100], [200, 100]] |
| 58 | + |
| 59 | +输出: [100, 100] |
| 60 | + |
| 61 | +解释: 相邻的方块不会过早地卡住,只有它们的底部边缘才能粘在表面上。 |
| 62 | +``` |
| 63 | + |
| 64 | +注意: |
| 65 | +* 1ドル <= positions.length <= 1000$ |
| 66 | +* 1ドル <= positions[i][0] <= 10^8$ |
| 67 | +* 1ドル <= positions[i][1] <= 10^6$ |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +### 基本分析 |
| 72 | + |
| 73 | +为了方便,我们使用 `ps` 来代指 `positions`。 |
| 74 | + |
| 75 | +每次从插入操作都附带一次询问,因此询问次数为 1ドルe3,ドル左端点的最大值为 10ドルe8,ドル边长最大值为 1ドルe6,ドル由此可知值域范围大于 1ドルe8,ドル但不超过 1ドルe9$。 |
| 76 | + |
| 77 | +对于值域范围大,但查询次数有限的区间和问题,不久前曾经总结过 : [求解常见「值域爆炸,查询有限」区间问题的几种方式](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491187&idx=2&sn=bb2d8b7e89c535914da8107387e951a2),可作为前置 🧀 进行了解。 |
| 78 | + |
| 79 | +而我的个人习惯,一般要么使用「离散化 + 线段树」,要么使用「线段树(动态开点)」进行求解。 |
| 80 | + |
| 81 | +本题为「非强制在线」问题,因此可以先对 `ps` 数组进行离散化,将值域映射到较小的空间,然后套用固定占用 4ドル \times n$ 空间的线段树求解。 |
| 82 | + |
| 83 | +但更为灵活(能够同时应对强制在线问题)的求解方式是「线段树(动态开点)」。 |
| 84 | + |
| 85 | +同时实现动态开点的方式有两种: |
| 86 | + |
| 87 | +1. 根据操作次数对使用到的最大点数进行预估,并采用数组方式进行实现线段树(解法一); |
| 88 | +2. 使用动态指针(解法二); |
| 89 | + |
| 90 | +方式一在不久之前的每日一题 [933. 最近的请求次数](https://sharingsource.github.io/2022/05/06/933.%20%E6%9C%80%E8%BF%91%E7%9A%84%E8%AF%B7%E6%B1%82%E6%AC%A1%E6%95%B0%EF%BC%88%E7%AE%80%E5%8D%95%EF%BC%89/) 讲过,因此今天把方式二也写一下。 |
| 91 | + |
| 92 | +具体的,我们将顺序放置方块的操作(假设当前方块的左端点为 $a,ドル边长为 $len,ドル则有右端点为 $b = a + len$),分成如下两步进行: |
| 93 | + |
| 94 | +* 查询当前范围 $[a, b]$ 的最大高度为多少,假设为 $cur$; |
| 95 | +* 更新当前范围 $[a, b]$ 的最新高度为 $cur + len$。 |
| 96 | + |
| 97 | +因此这本质上是一个「区间修改 + 区间查询」的问题,我们需要实现带「懒标记」的线段树,从而确保在进行「区间修改」时复杂度仍为 $O(\log{n})$。 |
| 98 | + |
| 99 | +> 另外有一个需要注意的细节是:不同方块之间的边缘可以重合,但不会导致方块叠加,因此我们当我们对一个区间 $[a, b]$ 进行操作(查询或插入)时,可以将其调整为 $[a, b - 1],ドル从而解决边缘叠加操作高度错误的问题。 |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +### 线段树(动态开点 - 估点) |
| 104 | + |
| 105 | +估点的基本方式在前置 🧀 [求解常见「值域爆炸,查询有限」区间问题的几种方式](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247491187&idx=2&sn=bb2d8b7e89c535914da8107387e951a2) 详细讲过。 |
| 106 | + |
| 107 | +简单来说,可以直接估算为 6ドル \times m \times \log{n}$ 即可,其中 $m$ 为询问次数(对应本题就是 `ps` 的长度),而 $n$ 为值域大小(对应本题可直接取成 1ドルe9$);而另外一个比较实用(避免估算)的估点方式可以「尽可能的多开点数」,利用题目给定的空间上界和我们创建的自定义类(结构体)的大小,尽可能的多开(不考虑字节对齐,或者结构体过大的情况,`Java` 的 128ドルM$ 可以开到 5ドル \times 10^6$ 以上)。 |
| 108 | + |
| 109 | +代码: |
| 110 | +```Java |
| 111 | +class Solution { |
| 112 | + class Node { |
| 113 | + // ls 和 rs 分别代表当前区间的左右子节点所在 tr 数组中的下标 |
| 114 | + // val 代表当前区间的最大高度,add 为懒标记 |
| 115 | + int ls, rs, val, add; |
| 116 | + } |
| 117 | + int N = (int)1e9, cnt = 0; |
| 118 | + Node[] tr = new Node[1000010]; |
| 119 | + void update(int u, int lc, int rc, int l, int r, int v) { |
| 120 | + if (l <= lc && rc <= r) { |
| 121 | + tr[u].val = v; |
| 122 | + tr[u].add = v; |
| 123 | + return ; |
| 124 | + } |
| 125 | + pushdown(u); |
| 126 | + int mid = lc + rc >> 1; |
| 127 | + if (l <= mid) update(tr[u].ls, lc, mid, l, r, v); |
| 128 | + if (r > mid) update(tr[u].rs, mid + 1, rc, l, r, v); |
| 129 | + pushup(u); |
| 130 | + } |
| 131 | + int query(int u, int lc, int rc, int l, int r) { |
| 132 | + if (l <= lc && rc <= r) return tr[u].val; |
| 133 | + pushdown(u); |
| 134 | + int mid = lc + rc >> 1, ans = 0; |
| 135 | + if (l <= mid) ans = query(tr[u].ls, lc, mid, l, r); |
| 136 | + if (r > mid) ans = Math.max(ans, query(tr[u].rs, mid + 1, rc, l, r)); |
| 137 | + return ans; |
| 138 | + } |
| 139 | + void pushdown(int u) { |
| 140 | + if (tr[u] == null) tr[u] = new Node(); |
| 141 | + if (tr[u].ls == 0) { |
| 142 | + tr[u].ls = ++cnt; |
| 143 | + tr[tr[u].ls] = new Node(); |
| 144 | + } |
| 145 | + if (tr[u].rs == 0) { |
| 146 | + tr[u].rs = ++cnt; |
| 147 | + tr[tr[u].rs] = new Node(); |
| 148 | + } |
| 149 | + if (tr[u].add == 0) return ; |
| 150 | + int add = tr[u].add; |
| 151 | + tr[tr[u].ls].add = add; tr[tr[u].rs].add = add; |
| 152 | + tr[tr[u].ls].val = add; tr[tr[u].rs].val = add; |
| 153 | + tr[u].add = 0; |
| 154 | + } |
| 155 | + void pushup(int u) { |
| 156 | + tr[u].val = Math.max(tr[tr[u].ls].val, tr[tr[u].rs].val); |
| 157 | + } |
| 158 | + public List<Integer> fallingSquares(int[][] ps) { |
| 159 | + List<Integer> ans = new ArrayList<>(); |
| 160 | + tr[1] = new Node(); |
| 161 | + for (int[] info : ps) { |
| 162 | + int x = info[0], h = info[1], cur = query(1, 1, N, x, x + h - 1); |
| 163 | + update(1, 1, N, x, x + h - 1, cur + h); |
| 164 | + ans.add(tr[1].val); |
| 165 | + } |
| 166 | + return ans; |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | +* 时间复杂度:令 $m$ 为查询次数,$n$ 为值域大小,复杂度为 $O(m\log{n})$ |
| 171 | +* 空间复杂度:$O(m\log{n})$ |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +### 线段树(动态开点 - 动态指针) |
| 176 | + |
| 177 | +利用「动态指针」实现的「动态开点」可以有效避免数组估点问题,更重要的是可以有效避免 `new` 大数组的初始化开销,对于 LC 这种还跟你算所有样例总时长的 OJ 来说,在不考虑 `static` 优化/全局数组优化 的情况下,动态指针的方式要比估点的方式来得好。 |
| 178 | + |
| 179 | +代码: |
| 180 | +```Java |
| 181 | +class Solution { |
| 182 | + int N = (int)1e9; |
| 183 | + class Node { |
| 184 | + // ls 和 rs 分别代表当前区间的左右子节点 |
| 185 | + Node ls, rs; |
| 186 | + // val 代表当前区间的最大高度,add 为懒标记 |
| 187 | + int val, add; |
| 188 | + } |
| 189 | + Node root = new Node(); |
| 190 | + void update(Node node, int lc, int rc, int l, int r, int v) { |
| 191 | + if (l <= lc && rc <= r) { |
| 192 | + node.add = v; |
| 193 | + node.val = v; |
| 194 | + return ; |
| 195 | + } |
| 196 | + pushdown(node); |
| 197 | + int mid = lc + rc >> 1; |
| 198 | + if (l <= mid) update(node.ls, lc, mid, l, r, v); |
| 199 | + if (r > mid) update(node.rs, mid + 1, rc, l, r, v); |
| 200 | + pushup(node); |
| 201 | + } |
| 202 | + int query(Node node, int lc, int rc, int l, int r) { |
| 203 | + if (l <= lc && rc <= r) return node.val; |
| 204 | + pushdown(node); |
| 205 | + int mid = lc + rc >> 1, ans = 0; |
| 206 | + if (l <= mid) ans = query(node.ls, lc, mid, l, r); |
| 207 | + if (r > mid) ans = Math.max(ans, query(node.rs, mid + 1, rc, l, r)); |
| 208 | + return ans; |
| 209 | + } |
| 210 | + void pushdown(Node node) { |
| 211 | + if (node.ls == null) node.ls = new Node(); |
| 212 | + if (node.rs == null) node.rs = new Node(); |
| 213 | + if (node.add == 0) return ; |
| 214 | + node.ls.add = node.add; node.rs.add = node.add; |
| 215 | + node.ls.val = node.add; node.rs.val = node.add; |
| 216 | + node.add = 0; |
| 217 | + } |
| 218 | + void pushup(Node node) { |
| 219 | + node.val = Math.max(node.ls.val, node.rs.val); |
| 220 | + } |
| 221 | + public List<Integer> fallingSquares(int[][] ps) { |
| 222 | + List<Integer> ans = new ArrayList<>(); |
| 223 | + for (int[] info : ps) { |
| 224 | + int x = info[0], h = info[1], cur = query(root, 0, N, x, x + h - 1); |
| 225 | + update(root, 0, N, x, x + h - 1, cur + h); |
| 226 | + ans.add(root.val); |
| 227 | + } |
| 228 | + return ans; |
| 229 | + } |
| 230 | +} |
| 231 | +``` |
| 232 | +* 时间复杂度:令 $m$ 为查询次数,$n$ 为值域大小,复杂度为 $O(m\log{n})$ |
| 233 | +* 空间复杂度:$O(m\log{n})$ |
| 234 | + |
| 235 | +--- |
| 236 | + |
| 237 | +### 最后 |
| 238 | + |
| 239 | +这是我们「刷穿 LeetCode」系列文章的第 `No.699` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 240 | + |
| 241 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 242 | + |
| 243 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 244 | + |
| 245 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 246 | + |
0 commit comments