@@ -49,81 +49,81 @@ https://leetcode-cn.com/problems/minimum-number-of-increments-on-subarrays-to-fo
49
49
50
50
## 前置知识
51
51
52
- - 差分与前缀和
52
+ -
53
53
54
54
## 公司
55
55
56
56
- 暂无
57
57
58
58
## 思路
59
59
60
- 首先我们要有前缀和以及差分的知识。这里简单讲述一下:
61
60
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 的数组, 这是等价的。(不这么思考问题也不大,只不过会稍微方便一点罢了)
64
62
65
- 前缀和与差分数组互为逆运算。如何理解呢?这里的原因在于你对 A 的差分数组 d 求前缀和就是数组 A。前缀和对于求区间和有重大意义。而差分数组通常用于 ** 先对数组的若干区间执行若干次增加或者减少操作 ** 。仔细看这道题不就是 ** 对数组若干区间执行 n 次增加操作 ** ,让你返回从一个数组到另外一个数组的最少操作次数么?差分数组对两个数字的操作等价于原始数组区间操作,这样时间复杂度大大降低 O(N) -> O(1) 。
63
+ 而我们可以进行的操作是选择一个 ** 子数组 ** ,将子数组中的每个元素减去 1(题目是加 1, 但是我们是反着思考,那么就是减去 1) 。
66
64
67
- 题目要求 ** 返回从 initial 得到 target 的最少操作次数 ** 。这道题我们可以逆向思考 ** 返回从 target 得到 initial 的最少操作次数 ** 。
65
+ 考虑 nums [ 0 ] :
68
66
69
- 这有什么区别么?对问题求解有什么帮助?由于 initial 是全为 0 的数组,如果将其作为最终搜索状态则不需要对状态进行额外的判断。这句话可能比较难以理解,我举个例子你就懂了。比如我不反向思考,那么初始状态就是 initial ,最终搜索状态自然是 target ,假如我们现在搜索到一个状态 state.我们需要** 逐个判断 state[ i] 是否等于 target[ i] ** ,如果全部都相等则说明搜索到了 target ,否则没有搜索到,我们继续搜索。而如果我们从 target 开始搜,最终状态就是 initial,我们只需要判断每一位是否都是 0 就好了。 这算是搜索问题的常用套路。
67
+ - 其如果是 0,我们没有必要对其进行修改。
68
+ - 如果 nums[ 0] > 0,我们需要进行 nums[ i] 次操作将其变为 0
70
69
71
- 上面讲到了对差分数组求前缀和可以还原原数组,这是差分数组的性质决定的。这里还有一个特点是 ** 如果差分数组是全 0 数组,比如 [ 0, 0, 0, 0 ] ,那么原数组也是 [ 0, 0, 0, 0 ] ** 。因此将 target 的差分数组 d 变更为 全为 0 的数组就等价于 target 变更为 initaial。
70
+ 由于每次操作都可以选择一个子数组,而不是一个数。考虑这次修改的区间为 [ l, r ] ,这里 l 自然就是 0,那么 r 取多少可以使得结果最佳呢?
72
71
73
- 如何将 target 变更为 initaial?
72
+ > 我们用 [ l, r ] 来描述一次操作将 nums [ l...r ] (l和r都包含) 的元素减去 1 的操作。
74
73
75
- 由于我们是反向操作,也就是说我们可执行的操作是 ** -1 ** ,反映在差分数组上就是在 d 的左端点 -1,右端点(可选)+1。如果没有对应的右端点+1 也是可以的。这相当于给原始数组的 [ i,n-1 ] +1,其中 n 为 A 的长度 。
74
+ 这实际上取决于 nums [ 1 ] , nums [ 2 ] 的取值 。
76
75
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] 进行操作。
78
78
79
- ```
80
- [3, -2, 0, 1] -> [**2**, **-1**, 0, 1] -> [**1**, **0**, 0, 1] -> [**0**, 0, 0, 1] -> [0, 0, 0, **0**]
81
- ```
79
+ 我们的目的就是减少操作数,因此我们可以贪心地求最少操作数。具体为:
82
80
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 步骤。
84
86
85
- 至此,我们的算法就比较明了了 。
87
+ 总的思路就是先选最左边不为 0 的位置为左端点,然后 ** 尽可能延伸右端点 ** ,每次确定右端点的时候,我们需要找到 nums [ i...r ] 的最小值,然后将 nums [ i...r ] 减去这个最小值。这里的”尽可能延伸“就是没有遇到 num [ j ] == 0 的点 。
86
88
87
- 具体算法:
89
+ 这种做法的时间复杂度为 $O(n^2)$。而数据范围为 $10^5$,因此这种做法是不可以接受的。
88
90
89
- - 对 A 计算差分数组 d
90
- - 遍历差分数组 d,对 d 中 大于 0 的求和。该和就是答案。
91
+ > 不懂为什么不可以接受,可以看下我的这篇文章:https://lucifer.ren/blog/2020/12/21/shuati-silu3/
91
92
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
+ 我们接下来考虑如何优化。
104
94
105
- ** 复杂度分析 ** 令 N 为数组长度 。
95
+ 对于 nums [ i ] > 0,我们确定了左端点为 i 后,我们需要确定具体右端点 r 只是为了更新 nums [ i...r ] 的值。而更新这个值的目的就是想知道它们还需要几次操作。我们考虑如何将这个过程优化 。
106
96
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] 就是一片木桶。
109
105
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 个操作具体是左边哪一个操作带来的,因为题目只是让你求一个次数,而不是具体的操作序列。
111
110
112
111
## 关键点
113
112
114
113
- 逆向思考
115
- - 使用差分减少时间复杂度
114
+ - 考虑修改的左右端点
116
115
117
116
## 代码
118
117
119
118
代码支持:Python3
120
119
121
120
``` python
122
121
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 ])
127
127
return ans
128
128
```
129
129
@@ -132,6 +132,10 @@ class Solution:
132
132
- 时间复杂度:$O(N)$
133
133
- 空间复杂度:$O(1)$
134
134
135
+ ## 相似题目
136
+
137
+ - [ 3229. 使数组等于目标数组所需的最少操作次数] ( ./3229.minimum-operations-to-make-array-equal-to-target.md )
138
+
135
139
## 扩展
136
140
137
141
如果题目改为:给你一个数组 nums,以及 size 和 K。 其中 size 指的是你不能对区间大小为 size 的子数组执行+1 操作,而不是上面题目的** 任意** 子数组。K 指的是你只能进行 K 次 +1 操作,而不是上面题目的任意次。题目让你求的是** 经过这样的 k 次+1 操作,数组 nums 的最小值最大可以达到多少** 。
0 commit comments