Skip to content

Commit 5f7ad64

Browse files
author
robot
committed
feat: $3229
1 parent d301385 commit 5f7ad64

File tree

1 file changed

+44
-40
lines changed

1 file changed

+44
-40
lines changed

problems/1526.minimum-number-of-increments-on-subarrays-to-form-a-target-array.md

+44-40
Original file line numberDiff line numberDiff line change
@@ -49,81 +49,81 @@ https://leetcode-cn.com/problems/minimum-number-of-increments-on-subarrays-to-fo
4949

5050
## 前置知识
5151

52-
- 差分与前缀和
52+
-
5353

5454
## 公司
5555

5656
- 暂无
5757

5858
## 思路
5959

60-
首先我们要有前缀和以及差分的知识。这里简单讲述一下:
6160

62-
- 前缀和 pres:对于一个数组 A [1,2,3,4],它的前缀和就是 [1,1+2,1+2+3,1+2+3+4],也就是 [1,3,6,10],也就是说前缀和 $pres[i] =\sum_{n=0}^{n=i}A[i]$
63-
- 差分数组 d:对于一个数组 A [1,2,3,4],它的差分数组就是 [1,2-1,3-2,4-3],也就是 [1,1,1,1],也就是说差分数组 $d[i] = A[i] - A[i-1](i > 0)$,$d[i] = A[i](i == 0)$
61+
这道题是要我们将一个全为 0 的数组修改为 nums 数组。我们不妨反着思考,将 nums 改为一个长度相同且全为 0 的数组, 这是等价的。(不这么思考问题也不大,只不过会稍微方便一点罢了)
6462

65-
前缀和与差分数组互为逆运算。如何理解呢?这里的原因在于你对 A 的差分数组 d 求前缀和就是数组 A。前缀和对于求区间和有重大意义。而差分数组通常用于**先对数组的若干区间执行若干次增加或者减少操作**。仔细看这道题不就是**对数组若干区间执行 n 次增加操作**,让你返回从一个数组到另外一个数组的最少操作次数么?差分数组对两个数字的操作等价于原始数组区间操作,这样时间复杂度大大降低 O(N) -> O(1)
63+
而我们可以进行的操作是选择一个**子数组**,将子数组中的每个元素减去 1(题目是加 1, 但是我们是反着思考,那么就是减去 1)
6664

67-
题目要求**返回从 initial  得到   target  的最少操作次数**。这道题我们可以逆向思考**返回从 target  得到  initial   的最少操作次数**
65+
考虑 nums[0]
6866

69-
这有什么区别么?对问题求解有什么帮助?由于  initial 是全为 0 的数组,如果将其作为最终搜索状态则不需要对状态进行额外的判断。这句话可能比较难以理解,我举个例子你就懂了。比如我不反向思考,那么初始状态就是 initial ,最终搜索状态自然是 target ,假如我们现在搜索到一个状态 state.我们需要**逐个判断 state[i] 是否等于 target[i]**,如果全部都相等则说明搜索到了 target ,否则没有搜索到,我们继续搜索。而如果我们从 target  开始搜,最终状态就是  initial,我们只需要判断每一位是否都是 0 就好了。 这算是搜索问题的常用套路。
67+
- 其如果是 0,我们没有必要对其进行修改。
68+
- 如果 nums[0] > 0,我们需要进行 nums[i] 次操作将其变为 0
7069

71-
上面讲到了对差分数组求前缀和可以还原原数组,这是差分数组的性质决定的。这里还有一个特点是**如果差分数组是全 0 数组,比如[0, 0, 0, 0],那么原数组也是[0, 0, 0, 0]**。因此将 target 的差分数组 d 变更为 全为 0 的数组就等价于 target 变更为 initaial。
70+
由于每次操作都可以选择一个子数组,而不是一个数。考虑这次修改的区间为 [l, r],这里 l 自然就是 0,那么 r 取多少可以使得结果最佳呢?
7271

73-
如何将 target 变更为 initaial?
72+
> 我们用 [l, r] 来描述一次操作将 nums[l...r](l和r都包含) 的元素减去 1 的操作。
7473
75-
由于我们是反向操作,也就是说我们可执行的操作是 **-1**,反映在差分数组上就是在 d 的左端点 -1,右端点(可选)+1。如果没有对应的右端点+1 也是可以的。这相当于给原始数组的 [i,n-1] +1,其中 n 为 A 的长度
74+
这实际上取决于 nums[1], nums[2] 的取值
7675

77-
如下是一种将 [3, -2, 0, 1] 变更为 [0, 0, 0, 0] 的可能序列。
76+
- 如果 nums[1] > 0,那么我们需要对 nums[1] 进行 nums[1] 次操作。(这个操作可能是 l 为 1 的,也可能是 r > 1 的)
77+
- 如果 nums[1] == 0,那么我们不需要对 nums[1] 进行操作。
7878

79-
```
80-
[3, -2, 0, 1] -> [**2**, **-1**, 0, 1] -> [**1**, **0**, 0, 1] -> [**0**, 0, 0, 1] -> [0, 0, 0, **0**]
81-
```
79+
我们的目的就是减少操作数,因此我们可以贪心地求最少操作数。具体为:
8280

83-
可以看出,上面需要进行四次区间操作,因此我们需要返回 4。
81+
1. 找到第一个满足 nums[i] != 0 的位置 i
82+
2. 先将操作的左端点固定为 i,然后选择右端点 r。对于端点 r,我们需要****操作 k 次操作,其中 k 为 min(nums[r], nums[r - 1], ..., nums[i]) 。最小值可以在遍历的同时求出来。
83+
3. 此时 nums[i] 变为了 nums[i] - k, nums[i + 1] 变为了 nums[i + 1] - k,...,nums[r] 变为了 nums[r] - k。**由于最小值 k 为0零,会导致我们白白计算一圈,没有意义,因此我们只能延伸到不为 0 的点**
84+
4. 答案加 k,我们继续使用同样的方法确定右端点 r。
85+
5. i = i + 1,重复 2-4 步骤。
8486

85-
至此,我们的算法就比较明了了
87+
总的思路就是先选最左边不为 0 的位置为左端点,然后**尽可能延伸右端点**,每次确定右端点的时候,我们需要找到 nums[i...r] 的最小值,然后将 nums[i...r] 减去这个最小值。这里的”尽可能延伸“就是没有遇到 num[j] == 0 的点
8688

87-
具体算法:
89+
这种做法的时间复杂度为 $O(n^2)$。而数据范围为 $10^5$,因此这种做法是不可以接受的。
8890

89-
- 对 A 计算差分数组 d
90-
- 遍历差分数组 d,对 d 中 大于 0 的求和。该和就是答案。
91+
> 不懂为什么不可以接受,可以看下我的这篇文章:https://lucifer.ren/blog/2020/12/21/shuati-silu3/
9192
92-
```py
93-
class Solution:
94-
def minNumberOperations(self, A: List[int]) -> int:
95-
d = [A[0]]
96-
ans = 0
97-
98-
for i in range(1, len(A)):
99-
d.append(A[i] - A[i-1])
100-
for a in d:
101-
ans += max(0, a)
102-
return ans
103-
```
93+
我们接下来考虑如何优化。
10494

105-
**复杂度分析** 令 N 为数组长度
95+
对于 nums[i] > 0,我们确定了左端点为 i 后,我们需要确定具体右端点 r 只是为了更新 nums[i...r] 的值。而更新这个值的目的就是想知道它们还需要几次操作。我们考虑如何将这个过程优化
10696

107-
- 时间复杂度:$O(N)$
108-
- 空间复杂度:$O(N)$
97+
考虑 nums[i+1] 和 nums[i] 的关系:
98+
99+
- 如果 nums[i+1] > nums[i],那么我们还需要对 nums[i+1] 进行 nums[i+1] - nums[i] 次操作。
100+
- 如果 nums[i+1] <= nums[i],那么我们不需要对 nums[i+1] 进行操作。
101+
102+
如果我们可以把 [i,r]的操作信息从 i 更新到 i + 1 的位置,那是不是说后面的数只需要看前面相邻的数就行了?
103+
104+
我们可以想象 nums[i+1] 就是一片木桶。
109105

110-
实际上,我们没有必要真实地计算差分数组 d,而是边遍历边求,也不需要对 d 进行存储。具体见下方代码区。
106+
- 如果 nums[i+1] 比 nums[i+2] 低,那么通过操作 [i,r] 其实也只能过来 nums[i+1] 这么多水。因此这个操作是从[i,r]还是[i+1,r]过来都无所谓。因为至少可以从左侧过来 nums[i+1] 的水。
107+
- 如果 nums[i+1] 比 nums[i+2] 高,那么我们也不必关心这个操作是 [i,r] 还是 [i+1,r]。因为既然 nums[i+1] 都已经变为 0 了,那么必然可以顺便把我搞定。
108+
109+
也就是说可以只考虑相邻两个数的关系,而不必考虑更远的数。而考虑的关键就是 nums[i] 能够从左侧的操作获得多少顺便操作的次数 m,nums[i] - m 就是我们需要额为的次数。我们不关心 m 个操作具体是左边哪一个操作带来的,因为题目只是让你求一个次数,而不是具体的操作序列。
111110

112111
## 关键点
113112

114113
- 逆向思考
115-
- 使用差分减少时间复杂度
114+
- 考虑修改的左右端点
116115

117116
## 代码
118117

119118
代码支持:Python3
120119

121120
```python
122121
class Solution:
123-
def minNumberOperations(self, A: List[int]) -> int:
124-
ans = A[0]
125-
for i in range(1, len(A)):
126-
ans += max(0, A[i] - A[i-1])
122+
def minNumberOperations(self, nums: List[int]) -> int:
123+
ans = abs(nums[0])
124+
for i in range(1, len(nums)):
125+
if abs(nums[i]) > abs(nums[i - 1]): # 这种情况,说明前面不能顺便把我改了,还需要我操作 k 次
126+
ans += abs(nums[i]) - abs(nums[i - 1])
127127
return ans
128128
```
129129

@@ -132,6 +132,10 @@ class Solution:
132132
- 时间复杂度:$O(N)$
133133
- 空间复杂度:$O(1)$
134134

135+
## 相似题目
136+
137+
- [3229. 使数组等于目标数组所需的最少操作次数](./3229.minimum-operations-to-make-array-equal-to-target.md)
138+
135139
## 扩展
136140

137141
如果题目改为:给你一个数组 nums,以及 size 和 K。 其中 size 指的是你不能对区间大小为 size 的子数组执行+1 操作,而不是上面题目的**任意**子数组。K 指的是你只能进行 K 次 +1 操作,而不是上面题目的任意次。题目让你求的是**经过这样的 k 次+1 操作,数组 nums 的最小值最大可以达到多少**

0 commit comments

Comments
 (0)