|
| 1 | +## 题目地址(768. 最多能完成排序的块 II) |
| 2 | + |
| 3 | +https://leetcode-cn.com/problems/max-chunks-to-make-sorted-ii/ |
| 4 | + |
| 5 | +## 题目描述 |
| 6 | + |
| 7 | +``` |
| 8 | +这个问题和“最多能完成排序的块”相似,但给定数组中的元素可以重复,输入数组最大长度为2000,其中的元素最大为10**8。 |
| 9 | +
|
| 10 | +arr是一个可能包含重复元素的整数数组,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。 |
| 11 | +
|
| 12 | +我们最多能将数组分成多少块? |
| 13 | +
|
| 14 | +示例 1: |
| 15 | +
|
| 16 | +输入: arr = [5,4,3,2,1] |
| 17 | +输出: 1 |
| 18 | +解释: |
| 19 | +将数组分成2块或者更多块,都无法得到所需的结果。 |
| 20 | +例如,分成 [5, 4], [3, 2, 1] 的结果是 [4, 5, 1, 2, 3],这不是有序的数组。 |
| 21 | +示例 2: |
| 22 | +
|
| 23 | +输入: arr = [2,1,3,4,4] |
| 24 | +输出: 4 |
| 25 | +解释: |
| 26 | +我们可以把它分成两块,例如 [2, 1], [3, 4, 4]。 |
| 27 | +然而,分成 [2, 1], [3], [4], [4] 可以得到最多的块数。 |
| 28 | +注意: |
| 29 | +
|
| 30 | +arr的长度在[1, 2000]之间。 |
| 31 | +arr[i]的大小在[0, 10**8]之间。 |
| 32 | +
|
| 33 | +``` |
| 34 | + |
| 35 | +## 前置知识 |
| 36 | + |
| 37 | +- 栈 |
| 38 | +- 队列 |
| 39 | + |
| 40 | +## 计数 |
| 41 | + |
| 42 | +### 思路 |
| 43 | + |
| 44 | +这里可以使用类似计数排序的技巧来完成。以题目给的 [2,1,3,4,4] 来说: |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | +可以先计数,比如用一个数组来计数,其中数组的索引表示值,数组的值表示其对应的出现次数。比如上面,除了 4 出现了两次,其他均出现一次,因此 count 就是 [0,1,1,1,2]。 |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | +其中 counts[4] 就是 2,表示的就是 4 这个值出现了两次。 |
| 53 | + |
| 54 | +> 实际上 count 最开始的 0 是没有必要的,不过这样方便理解罢了。 |
| 55 | +
|
| 56 | +如果我们使用数组来计数,那么空间复杂度就是 $upper - lower$,其中 upper 是 arr 的最大值, lower 是 arr 的最小值。 |
| 57 | + |
| 58 | +计数完毕之后, 我们要做的是比较当前的 arr 和最终的 arr(已经有序的 arr) 的计数数组的关系即可。 |
| 59 | + |
| 60 | +这里有一个关键点: **如果两个数组的计数信息是一致的,那么两个数组排序后的结果也是一致的。** 如果你理解计数排序,应该明白我的意思。不明白也没有关系, 我稍微解释一下你就懂了。 |
| 61 | + |
| 62 | +如果我把一个数组打乱,然后排序,得到的数组一定是确定的,即不管你怎么打乱排好序都是一个确定的有序序列。这个论点的正确性是毋庸置疑的。而实际上,一个数组无论怎么打乱,其计数结果也是确定的,这也是毋庸置疑的。反之,如果是两个不同的数组,打乱排序后的结果一定是不同的,计数也是同理。 |
| 63 | + |
| 64 | + |
| 65 | +(这两个数组排序后的结果以及计数信息是一致的) |
| 66 | + |
| 67 | +因此我们的算法有了: |
| 68 | + |
| 69 | +- 先排序 arr,不妨记排序后的 arr 为 sorted_arr |
| 70 | +- 从左到右遍历 arr,比如遍历到了索引为 i 的元素,其中 0 <= i < len(arr) |
| 71 | +- 如果 arr[:i+1] 的计数信息和 sorted_arr[:i+1] 的计数信息一致,那么说明可以分桶,否则不可以。 |
| 72 | + |
| 73 | +> arr[:i+1] 指的是 arr 的切片,从索引 0 到 索引 i 的一个切片。 |
| 74 | +
|
| 75 | +### 关键点 |
| 76 | + |
| 77 | +- 计数 |
| 78 | + |
| 79 | +### 代码 |
| 80 | + |
| 81 | +语言支持:Python |
| 82 | + |
| 83 | +```py |
| 84 | +class Solution(object): |
| 85 | + def maxChunksToSorted(self, arr): |
| 86 | + count_a = collections.defaultdict(int) |
| 87 | + count_b = collections.defaultdict(int) |
| 88 | + ans = 0 |
| 89 | + |
| 90 | + for a, b in zip(arr, sorted(arr)): |
| 91 | + count_a[a] += 1 |
| 92 | + count_b[b] += 1 |
| 93 | + if count_a == count_b: ans += 1 |
| 94 | + |
| 95 | + return ans |
| 96 | + |
| 97 | +``` |
| 98 | + |
| 99 | +**复杂度分析** |
| 100 | + |
| 101 | +- 时间复杂度:内部 count_a 和 count_b 的比较时间复杂度也是 $O(N)$,因此总的时间复杂度为 $O(N^2)$,其中 N 为数组长度。 |
| 102 | +- 空间复杂度:使用了两个 counter,其大小都是 N,因此空间复杂度为 $O(N)$,其中 N 为数组长度。 |
| 103 | + |
| 104 | +## 优化的计数 |
| 105 | + |
| 106 | +### 思路 |
| 107 | + |
| 108 | +实际上,我们不需要两个 counter ,而是使用一个 counter 来记录 arr 和 sorted_arr 的 diff 即可。但是这也仅仅是空间上的一个常数优化而已。 |
| 109 | + |
| 110 | +我们还可以在时间上进一步优化, 去除内部 count_a 和 count_b 的比较,这样算法的瓶颈就是排序了。而去除的关键点就是我们上面提到的**记录 diff**,具体参考下方代码。 |
| 111 | + |
| 112 | +### 关键点 |
| 113 | + |
| 114 | +- 计数 |
| 115 | +- count 的边界条件 |
| 116 | + |
| 117 | +### 代码 |
| 118 | + |
| 119 | +语言支持:Python |
| 120 | + |
| 121 | +```py |
| 122 | +class Solution(object): |
| 123 | + class Solution(object): |
| 124 | + def maxChunksToSorted(self, arr): |
| 125 | + count = collections.defaultdict(int) |
| 126 | + non_zero_cnt = 0 |
| 127 | + ans = 0 |
| 128 | + |
| 129 | + for a, b in zip(arr, sorted(arr)): |
| 130 | + if count[a] == -1: non_zero_cnt -= 1 |
| 131 | + if count[a] == 0: non_zero_cnt += 1 |
| 132 | + count[a] += 1 |
| 133 | + if count[b] == 1: non_zero_cnt -= 1 |
| 134 | + if count[b] == 0: non_zero_cnt += 1 |
| 135 | + count[b] -= 1 |
| 136 | + if non_zero_cnt == 0: ans += 1 |
| 137 | + |
| 138 | + return ans |
| 139 | + |
| 140 | +``` |
| 141 | + |
| 142 | +**复杂度分析** |
| 143 | + |
| 144 | +- 时间复杂度:瓶颈在于排序,因此时间复杂度为 $O(NlogN)$,其中 N 为数组长度。 |
| 145 | +- 空间复杂度:使用了一个 counter,其大小是 N,因此空间复杂度为 $O(N)$,其中 N 为数组长度。 |
| 146 | + |
| 147 | +## 单调栈 |
| 148 | + |
| 149 | +### 思路 |
| 150 | + |
| 151 | +通过题目给的三个例子,应该可以发现一些端倪。 |
| 152 | + |
| 153 | +- 如果 arr 是非递减的,那么答案为 1。 |
| 154 | +- 如果 arr 是非递增的,那么答案是 arr 的长度。 |
| 155 | + |
| 156 | +并且由于**只有分的块内部可以排序**,块与块之间的相对位置是不能变的。因此**直观上**我们的核心其实找到从左到右开始不减少(增加或者不变)的地方并分块。 |
| 157 | + |
| 158 | +比如对于 [5,4,3,2,1] 来说: |
| 159 | + |
| 160 | +- 5 的下一个是 4,比 5 小,因此如果分块,那么永远不能变成[1,2,3,4,5]。 |
| 161 | +- 同理,4 的下一个是 3,比 4 小,因此如果分块,那么永远不能变成[1,2,3,4,5]。 |
| 162 | +- 。。。 |
| 163 | + |
| 164 | +最后就是不能只能是整体是一个大块,我们返回 1 即可。 |
| 165 | + |
| 166 | +我们继续分析一个稍微复杂一点的,即题目给的 [2,1,3,4,4]。 |
| 167 | + |
| 168 | +- 2 的下一个是 1,比 2 小,不能分块。 |
| 169 | +- 1 的下一个是 3,比 1 大,可以分块。 |
| 170 | +- 3 的下一个是 4,比 3 大,可以分块。 |
| 171 | +- 4 的下一个是 4,一样大,可以分块。 |
| 172 | + |
| 173 | +因此答案就是 4,分别是: |
| 174 | + |
| 175 | +- [2,1] |
| 176 | +- [3] |
| 177 | +- [3] |
| 178 | +- [4] |
| 179 | + |
| 180 | +然而上面的算法步骤是不正确的,原因在于只考虑局部,没有考虑整体,比如 **[4,2,2,1,1]** 这样的测试用例,实际上只应该返回 1,原因是后面碰得到了 1,使得前面不应该分块。 |
| 181 | + |
| 182 | +因为把数组分成数个块,分别排序每个块后,组合所有的块就跟整个数组排序的结果一样,这就意味着后面块中的最小值一定大于前面块的最大值,这样才能保证分块有。因此直观上,我们又会觉得是不是”只要后面有较小值,那么前面大于它的都应该在一个块里面“,实际上的确如此。 |
| 183 | + |
| 184 | +有没有注意到我们一直在找下一个比当前小的元素?这就是一个信号,使用单调递增栈即可以空间换时间的方式解决。对单调栈不熟悉的小伙伴可以看下我的[单调栈专题](https://lucifer.ren/blog/2020/11/03/monotone-stack/) |
| 185 | + |
| 186 | +不过这还不够,我们要把思路逆转! |
| 187 | + |
| 188 | + |
| 189 | + |
| 190 | +> 这是《逆转裁判》 中经典的台词, 主角在深处绝境的时候,会突然冒出这句话,从而逆转思维,寻求突破口。 |
| 191 | +
|
| 192 | +这里的话,我们将思路逆转,不是分割区块,而是**融合区块**。 |
| 193 | + |
| 194 | +比如 [2,1,3,4,4],遍历到 1 的时候会发现 1 比 2 小,因此 2, 1 需要在一块,我们可以将 2 和 1 融合,并**重新压回栈**。那么融合成 1 还是 2 呢?答案是 2,因为 2 是瓶颈,这提示我们可以用一个递增栈来完成。 |
| 195 | + |
| 196 | +因此本质上**栈存储的每一个元素就代表一个块,而栈里面的每一个元素的值就是块的最大值**。 |
| 197 | + |
| 198 | +以 [2,1,3,4,4] 来说, stack 的变化过程大概是: |
| 199 | + |
| 200 | +- [2] |
| 201 | +- 1 被融合了,保持 [2] 不变 |
| 202 | +- [2,3] |
| 203 | +- [2,3,4] |
| 204 | +- [2,3,4,4] |
| 205 | + |
| 206 | +简单来说,就是**将一个减序列压缩合并成最该序列的最大的值**。 因此最终返回 stack 的长度就可以了。 |
| 207 | + |
| 208 | +具体算法参考代码区,注释很详细。 |
| 209 | + |
| 210 | +### 代码 |
| 211 | + |
| 212 | +语言支持:Python,CPP,Java,JS, Go, PHP |
| 213 | + |
| 214 | +```py |
| 215 | +class Solution: |
| 216 | + def maxChunksToSorted(self, A: [int]) -> int: |
| 217 | + stack = [] |
| 218 | + for a in A: |
| 219 | + # 遇到一个比栈顶小的元素,而前面的块不应该有比 a 小的 |
| 220 | + # 而栈中每一个元素都是一个块,并且栈的存的是块的最大值,因此栈中比 a 小的值都需要 pop 出来 |
| 221 | + if stack and stack[-1] > a: |
| 222 | + # 我们需要将融合后的区块的最大值重新放回栈 |
| 223 | + # 而 stack 是递增的,因此 stack[-1] 是最大的 |
| 224 | + cur = stack[-1] |
| 225 | + # 维持栈的单调递增 |
| 226 | + while stack and stack[-1] > a: stack.pop() |
| 227 | + stack.append(cur) |
| 228 | + else: |
| 229 | + stack.append(a) |
| 230 | + # 栈存的是块信息,因此栈的大小就是块的数量 |
| 231 | + return len(stack) |
| 232 | + |
| 233 | + |
| 234 | +``` |
| 235 | + |
| 236 | +CPP: |
| 237 | + |
| 238 | +```cpp |
| 239 | +class Solution { |
| 240 | +public: |
| 241 | + int maxChunksToSorted(vector<int>& arr) { |
| 242 | + stack<int> stack; |
| 243 | + for(int i =0;i<arr.size();i++){ |
| 244 | + // 遇到一个比栈顶小的元素,而前面的块不应该有比 a 小的 |
| 245 | + // 而栈中每一个元素都是一个块,并且栈的存的是块的最大值,因此栈中比 a 小的值都需要 pop 出来 |
| 246 | + if(!stack.empty()&&stack.top()>arr[i]){ |
| 247 | + // 我们需要将融合后的区块的最大值重新放回栈 |
| 248 | + // 而 stack 是递增的,因此 stack[-1] 是最大的 |
| 249 | + int cur = stack.top(); |
| 250 | + // 维持栈的单调递增 |
| 251 | + while(!stack.empty()&&stack.top()>arr[i]){ |
| 252 | + sstackta.pop(); |
| 253 | + } |
| 254 | + |
| 255 | + stack.push(cur); |
| 256 | + }else{ |
| 257 | + |
| 258 | + stack.push(arr[i]); |
| 259 | + } |
| 260 | + } |
| 261 | + // 栈存的是块信息,因此栈的大小就是块的数量 |
| 262 | + return stack.size(); |
| 263 | + } |
| 264 | +}; |
| 265 | +``` |
| 266 | + |
| 267 | +JAVA: |
| 268 | + |
| 269 | +```java |
| 270 | +class Solution { |
| 271 | + public int maxChunksToSorted(int[] arr) { |
| 272 | + LinkedList<Integer> stack = new LinkedList<Integer>(); |
| 273 | + for (int num : arr) { |
| 274 | + // 遇到一个比栈顶小的元素,而前面的块不应该有比 a 小的 |
| 275 | + // 而栈中每一个元素都是一个块,并且栈的存的是块的最大值,因此栈中比 a 小的值都需要 pop 出来 |
| 276 | + if (!stack.isEmpty() && num < stack.getLast()) { |
| 277 | + // 我们需要将融合后的区块的最大值重新放回栈 |
| 278 | + // 而 stack 是递增的,因此 stack[-1] 是最大的 |
| 279 | + int cur = stack.removeLast(); |
| 280 | + // 维持栈的单调递增 |
| 281 | + while (!stack.isEmpty() && num < stack.getLast()) { |
| 282 | + stack.removeLast(); |
| 283 | + } |
| 284 | + stack.addLast(cur); |
| 285 | + } else { |
| 286 | + stack.addLast(num); |
| 287 | + } |
| 288 | + } |
| 289 | + // 栈存的是块信息,因此栈的大小就是块的数量 |
| 290 | + return stack.size(); |
| 291 | + } |
| 292 | +} |
| 293 | +``` |
| 294 | + |
| 295 | +JS: |
| 296 | + |
| 297 | +```js |
| 298 | +var maxChunksToSorted = function (arr) { |
| 299 | + const stack = []; |
| 300 | + |
| 301 | + for (let i = 0; i < arr.length; i++) { |
| 302 | + a = arr[i]; |
| 303 | + if (stack.length > 0 && stack[stack.length - 1] > a) { |
| 304 | + const cur = stack[stack.length - 1]; |
| 305 | + while (stack && stack[stack.length - 1] > a) stack.pop(); |
| 306 | + stack.push(cur); |
| 307 | + } else { |
| 308 | + stack.push(a); |
| 309 | + } |
| 310 | + } |
| 311 | + return stack.length; |
| 312 | +}; |
| 313 | +``` |
| 314 | + |
| 315 | +Go: |
| 316 | + |
| 317 | +```go |
| 318 | +func maxChunksToSorted(arr []int) int { |
| 319 | + var stack []int // 单调递增栈, stack[-1] 栈顶 |
| 320 | + for _, a := range arr { |
| 321 | + // 遇到一个比栈顶小的元素,而前面的块不应该有比 a 小的 |
| 322 | + // 而栈中每一个元素都是一个块,并且栈的存的是块的最大值,因此栈中比 a 小的值都需要 pop 出来 |
| 323 | + if len(stack) > 0 && stack[len(stack)-1] > a { |
| 324 | + // 我们需要将融合后的区块的最大值重新放回栈 |
| 325 | + // 而 stack 是递增的,因此 stack[-1] 是最大的 |
| 326 | + cur := stack[len(stack)-1] |
| 327 | + // 维持栈的单调递增 |
| 328 | + for len(stack) > 0 && stack[len(stack)-1] > a { |
| 329 | + stack = stack[:len(stack)-1] // pop |
| 330 | + } |
| 331 | + stack = append(stack, cur) // push |
| 332 | + } else { |
| 333 | + stack = append(stack, a) // push |
| 334 | + } |
| 335 | + } |
| 336 | + // 栈存的是块信息,因此栈的大小就是块的数量 |
| 337 | + return len(stack) |
| 338 | +} |
| 339 | +``` |
| 340 | + |
| 341 | +PHP: |
| 342 | + |
| 343 | +```php |
| 344 | +class Solution |
| 345 | +{ |
| 346 | + |
| 347 | + /** |
| 348 | + * @param Integer[] $arr |
| 349 | + * @return Integer |
| 350 | + */ |
| 351 | + function maxChunksToSorted($arr) |
| 352 | + { |
| 353 | + $stack = []; // 单调递增栈, stack[-1] 栈顶 |
| 354 | + foreach ($arr as $a) { |
| 355 | + // 遇到一个比栈顶小的元素,而前面的块不应该有比 a 小的 |
| 356 | + // 而栈中每一个元素都是一个块,并且栈的存的是块的最大值,因此栈中比 a 小的值都需要 pop 出来 |
| 357 | + if ($stack && $stack[count($stack) - 1] > $a) { |
| 358 | + $cur = $stack[count($stack) - 1]; |
| 359 | + // 维持栈的单调递增 |
| 360 | + while ($stack && $stack[count($stack) - 1] > $a) array_pop($stack); |
| 361 | + array_push($stack, $cur); |
| 362 | + } else array_push($stack, $a); |
| 363 | + } |
| 364 | + // 栈存的是块信息,因此栈的大小就是块的数量 |
| 365 | + return count($stack); |
| 366 | + } |
| 367 | +} |
| 368 | +``` |
| 369 | + |
| 370 | +**复杂度分析** |
| 371 | + |
| 372 | +- 时间复杂度:$O(N)$,其中 N 为数组长度。 |
| 373 | +- 空间复杂度:$O(N)$,其中 N 为数组长度。 |
| 374 | + |
| 375 | +## 总结 |
| 376 | + |
| 377 | +实际上本题的单调栈思路和 [【力扣加加】从排序到线性扫描(57. 插入区间)](https://leetcode-cn.com/problems/insert-interval/solution/li-kou-jia-jia-cong-pai-xu-dao-xian-xing-sao-miao-/) 以及 [394. 字符串解码](https://github.com/leetcode-pp/91alg-2/blob/master/solution/basic/d4.394.decode-string.md) 都有部分相似,大家可以结合起来理解。 |
| 378 | + |
| 379 | +融合与[【力扣加加】从排序到线性扫描(57. 插入区间)](https://leetcode-cn.com/problems/insert-interval/solution/li-kou-jia-jia-cong-pai-xu-dao-xian-xing-sao-miao-/) 相似, 重新压栈和 [394. 字符串解码](https://github.com/leetcode-pp/91alg-2/blob/master/solution/basic/d4.394.decode-string.md) 相似。 |
| 380 | + |
| 381 | +大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 37K star 啦。 |
| 382 | + |
| 383 | +大家也可以关注我的公众号《力扣加加》带你啃下算法这块硬骨头。 |
0 commit comments