|
| 1 | +## 题目地址(1526. 形成目标数组的子数组最少增加次数) |
| 2 | + |
| 3 | +https://leetcode-cn.com/problems/minimum-number-of-increments-on-subarrays-to-form-a-target-array/ |
| 4 | + |
| 5 | +## 题目描述 |
| 6 | + |
| 7 | +``` |
| 8 | +给你一个整数数组 target 和一个数组 initial ,initial 数组与 target 数组有同样的维度,且一开始全部为 0 。 |
| 9 | +
|
| 10 | +请你返回从 initial 得到 target 的最少操作次数,每次操作需遵循以下规则: |
| 11 | +
|
| 12 | +在 initial 中选择 任意 子数组,并将子数组中每个元素增加 1 。 |
| 13 | +答案保证在 32 位有符号整数以内。 |
| 14 | +
|
| 15 | + |
| 16 | +
|
| 17 | +示例 1: |
| 18 | +
|
| 19 | +输入:target = [1,2,3,2,1] |
| 20 | +输出:3 |
| 21 | +解释:我们需要至少 3 次操作从 intial 数组得到 target 数组。 |
| 22 | +[0,0,0,0,0] 将下标为 0 到 4 的元素(包含二者)加 1 。 |
| 23 | +[1,1,1,1,1] 将下标为 1 到 3 的元素(包含二者)加 1 。 |
| 24 | +[1,2,2,2,1] 将下表为 2 的元素增加 1 。 |
| 25 | +[1,2,3,2,1] 得到了目标数组。 |
| 26 | +示例 2: |
| 27 | +
|
| 28 | +输入:target = [3,1,1,2] |
| 29 | +输出:4 |
| 30 | +解释:(initial)[0,0,0,0] -> [1,1,1,1] -> [1,1,1,2] -> [2,1,1,2] -> [3,1,1,2] (target) 。 |
| 31 | +示例 3: |
| 32 | +
|
| 33 | +输入:target = [3,1,5,4,2] |
| 34 | +输出:7 |
| 35 | +解释:(initial)[0,0,0,0,0] -> [1,1,1,1,1] -> [2,1,1,1,1] -> [3,1,1,1,1] |
| 36 | + -> [3,1,2,2,2] -> [3,1,3,3,2] -> [3,1,4,4,2] -> [3,1,5,4,2] (target)。 |
| 37 | +示例 4: |
| 38 | +
|
| 39 | +输入:target = [1,1,1,1] |
| 40 | +输出:1 |
| 41 | + |
| 42 | +
|
| 43 | +提示: |
| 44 | +
|
| 45 | +1 <= target.length <= 10^5 |
| 46 | +1 <= target[i] <= 10^5 |
| 47 | +
|
| 48 | +``` |
| 49 | + |
| 50 | +## 前置知识 |
| 51 | + |
| 52 | +- 差分与前缀和 |
| 53 | + |
| 54 | +## 公司 |
| 55 | + |
| 56 | +- 暂无 |
| 57 | + |
| 58 | +## 思路 |
| 59 | + |
| 60 | +首先我们要有前缀和以及查分的知识。这里简单讲述一下: |
| 61 | + |
| 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)$ |
| 64 | + |
| 65 | +前缀和与差分数组互为逆运算。如何理解呢?这里的原因在于你对 A 的差分数组 d 求前缀和就是数组 A。前缀和对于求区间和有重大意义。而差分数组通常用于**先对数组的若干区间执行若干次增加或者减少操作**。仔细看这道题不就是**对数组若干区间执行 n 次增加操作**,让你返回从一个数组到另外一个数组的最少操作次数么?差分数组对两个数字的操作等价于原始数组区间操作,这样时间复杂度大大降低 O(N) -> O(1)。 |
| 66 | + |
| 67 | +题目要求**返回从 initial 得到 target 的最少操作次数**。这道题我们可以逆向思考**返回从 target 得到 initial 的最少操作次数**。 |
| 68 | + |
| 69 | +这有什么区别么?对问题求解有什么帮助?由于 initial 是全为 0 的数组,如果将其作为最终搜索状态则不需要对状态进行额外的判断。这句话可能比较难以理解,我举个例子你就懂了。比如我不反向思考,那么初始状态就是 initial ,最终搜索状态自然是 target ,假如我们现在搜索到一个状态 state.我们需要**逐个判断 state[i] 是否等于 target[i]**,如果全部都相等则说明搜索到了 target ,否则没有搜索到,我们继续搜索。而如果我们从 target 开始搜,最终状态就是 initial,我们只需要判断每一位是否都是 0 就好了。 这算是搜索问题的常用套路。 |
| 70 | + |
| 71 | +上面讲到了对差分数组求前缀和可以还原原数组,这是差分数组的性质决定的。这里还有一个特点是**如果差分数组是全 0 数组,比如[0, 0, 0, 0],那么原数组也是[0, 0, 0, 0]**。因此将 target 的差分数组 d 变更为 全为 0 的数组就等价于 target 变更为 initaial。 |
| 72 | + |
| 73 | +如何将 target 变更为 initaial? |
| 74 | + |
| 75 | +由于我们是反向操作,也就是说我们可执行的操作是 **-1**,反映在差分数组上就是在 d 的左端点 -1,右端点(可选)+1。如果没有对应的右端点+1 也是可以的。这相当于给原始数组的 [i,n-1] +1,其中 n 为 A 的长度。 |
| 76 | + |
| 77 | +如下是一种将 [3, -2, 0, 1] 变更为 [0, 0, 0, 0] 的可能序列。 |
| 78 | + |
| 79 | +``` |
| 80 | +[3, -2, 0, 1] -> [**2**, **-1**, 0, 1] -> [**1**, **0**, 0, 1] -> [**0**, 0, 0, 1] -> [0, 0, 0, **0**] |
| 81 | +``` |
| 82 | + |
| 83 | +可以看出,上面需要进行四次区间操作,因此我们需要返回 4。 |
| 84 | + |
| 85 | +至此,我们的算法就比较明了了。 |
| 86 | + |
| 87 | +具体算法: |
| 88 | + |
| 89 | +- 对 A 计算差分数组 d |
| 90 | +- 遍历差分数组 d,对 d 中 大于 0 的求和。该和就是答案。 |
| 91 | + |
| 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 | +``` |
| 104 | + |
| 105 | +**复杂度分析** |
| 106 | +令 N 为数组长度。 |
| 107 | + |
| 108 | +- 时间复杂度:$O(N)$ |
| 109 | +- 空间复杂度:$O(N)$ |
| 110 | + |
| 111 | +实际上,我们没有必要真实地计算差分数组 d,而是边遍历边求,也不需要对 d 进行存储。具体见下方代码区。 |
| 112 | + |
| 113 | +## 关键点 |
| 114 | + |
| 115 | +- 逆向思考 |
| 116 | +- 使用差分减少时间复杂度 |
| 117 | + |
| 118 | +## 代码 |
| 119 | + |
| 120 | +代码支持:Python3 |
| 121 | + |
| 122 | +```python |
| 123 | +class Solution: |
| 124 | + def minNumberOperations(self, A: List[int]) -> int: |
| 125 | + ans = A[0] |
| 126 | + for i in range(1, len(A)): |
| 127 | + ans += max(0, A[i] - A[i-1]) |
| 128 | + return ans |
| 129 | +``` |
| 130 | + |
| 131 | +**复杂度分析** |
| 132 | +令 N 为数组长度。 |
| 133 | + |
| 134 | +- 时间复杂度:$O(N)$ |
| 135 | +- 空间复杂度:$O(1)$ |
| 136 | + |
| 137 | +## 扩展 |
| 138 | + |
| 139 | +如果题目改为:给你一个数组 nums,以及 size 和 K。 其中 size 指的是你不能对区间大小为 size 的子数组执行+1 操作,而不是上面题目的**任意**子数组。K 指的是你只能进行 K 次 +1 操作,而不是上面题目的任意次。题目让你求的是**经过这样的 k 次+1 操作,数组 nums 的最小值最大可以达到多少**。 |
| 140 | + |
| 141 | +比如: |
| 142 | + |
| 143 | +``` |
| 144 | +输入: |
| 145 | +nums = [1, 4, 1, 1, 6] |
| 146 | +size = 3 |
| 147 | +k = 2 |
| 148 | +
|
| 149 | +解释: |
| 150 | +将 [1, 4, 1] +1 得到 [2, 5, 2, 1, 6] ,对 [5, 2, 1] +1 得到 [2, 6, 3, 2, 6]. |
| 151 | +``` |
| 152 | + |
| 153 | +解决问题的关键有两点: |
| 154 | + |
| 155 | +- 定义函数 possible(target),其功能是**在 K 步之内,每次都只能对 size 大小的子数组+1,是否可以满足数组的最小值>=target**。 |
| 156 | +- 有了上面的铺垫。我们要找的其实就是满足 possible(target) 的最大的 target。 |
| 157 | + |
| 158 | +这里有个关键点,那就是 |
| 159 | + |
| 160 | +- 如果 possible(target)为 true。那么 target 以下的都不用看了,肯定都满足。 |
| 161 | +- 如果 possible(target)为 false。那么 target 以上的都不用看了,肯定都满足。 |
| 162 | + |
| 163 | +也就是说无论如何我们都能将解空间缩小一半,这提示我们使用二分法。结合前面的知识”我们要找的其实就是满足 possible(target) 的最大的 target“,可知道应该使用**最右二分**,如果对最右二分不熟悉的可以看下[二分讲义](https://github.com/azl397985856/leetcode/blob/master/91/binary-search.md) |
| 164 | + |
| 165 | +参考代码: |
| 166 | + |
| 167 | +```py |
| 168 | +class Solution: |
| 169 | + def solve(self, A, size, K): |
| 170 | + N = len(A) |
| 171 | + |
| 172 | + def possible(target): |
| 173 | + # 差分数组 d |
| 174 | + d = [0] * N |
| 175 | + moves = a = 0 |
| 176 | + for i in range(N): |
| 177 | + # a 相当于差分数组 d 的前缀和 |
| 178 | + a += d[i] |
| 179 | + # 当前值和 target 的差距 |
| 180 | + delta = target - (A[i] + a) |
| 181 | + # 大于 0 表示不到 target,我们必须需要进行 +1 操作 |
| 182 | + if delta > 0: |
| 183 | + moves += delta |
| 184 | + # 更新前缀和 |
| 185 | + a += delta |
| 186 | + # 如果 i + size >= N 对应我上面提到的只修改左端点,不修改右端点的情况 |
| 187 | + if i + size < N: |
| 188 | + d[i + size] -= delta |
| 189 | + # 执行的+1操作小于等于K 说明可行 |
| 190 | + return moves <= K |
| 191 | + # 定义解空间 |
| 192 | + lo, hi = min(A), max(A) + K |
| 193 | + # 最右二分模板 |
| 194 | + while lo <= hi: |
| 195 | + mi = (lo + hi) // 2 |
| 196 | + if possible(mi): |
| 197 | + lo = mi + 1 |
| 198 | + else: |
| 199 | + hi = mi - 1 |
| 200 | + return hi |
| 201 | +``` |
| 202 | + |
| 203 | +更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 37K star 啦。 |
| 204 | + |
| 205 | +关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。 |
| 206 | + |
| 207 | + |
0 commit comments