@@ -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
122121class 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