Skip to content

Commit 61d19d1

Browse files
committed
Create 768.max-chunks-to-make-sorted-ii.md
1 parent 5ae7698 commit 61d19d1

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
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+
![](https://tva1.sinaimg.cn/large/0081Kckwly1gkejc54ecwj30dh037mx6.jpg)
47+
48+
可以先计数,比如用一个数组来计数,其中数组的索引表示值,数组的值表示其对应的出现次数。比如上面,除了 4 出现了两次,其他均出现一次,因此 count 就是 [0,1,1,1,2]
49+
50+
![](https://tva1.sinaimg.cn/large/0081Kckwly1gkejnxnxx9j30h9052glw.jpg)
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+
![](https://tva1.sinaimg.cn/large/0081Kckwly1gkejta7rb5j30dm07baad.jpg)
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+
![](https://tva1.sinaimg.cn/large/0081Kckwly1gkekhonycnj30zk0i0782.jpg)
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

Comments
 (0)