LeetCode 每日一题 1526. 形成目标数组的子数组最少增加次数
1526. 形成目标数组的子数组最少增加次数
给定一个整数数组 target 和一个初始全为 0 的数组 initial,要求通过最少的操作次数将 initial 变为 target。每次操作可以选择任意子数组,并将子数组中的每个元素增加 1。
示例分析
-
示例 1:
- 输入:
target = [1,2,3,2,1] - 输出:
3 - 解释:通过 3 次操作从
[0,0,0,0,0]变为[1,2,3,2,1]。
- 输入:
-
示例 2:
- 输入:
target = [3,1,1,2] - 输出:
4 - 解释:通过 4 次操作从
[0,0,0,0]变为[3,1,1,2]。
- 输入:
-
示例 3:
- 输入:
target = [3,1,5,4,2] - 输出:
7 - 解释:通过 7 次操作从
[0,0,0,0,0]变为[3,1,5,4,2]。
- 输入:
-
示例 4:
- 输入:
target = [1,1,1,1] - 输出:
1 - 解释:通过 1 次操作从
[0,0,0,0]变为[1,1,1,1]。
- 输入:
题解
方法一:模拟(会超时)
从 0 开始构造 target 数组相当于将 target 数数组变为全 0
每次操作可以选择一段区间 [l,r] 进行 -1 ,直到变为全 0
对一段区间进行 -1 操作,可以使用差分数组快速实现
由于需要减去的总数字大小是固定,即target数据总和,为了得到最少操作数,每一次的操作区间自然是越大越好
但是由于操作过程中会出现 0 ,0-1=-1 不能全数据变为 0,所以区间内是不能包含 0 的,即操作区间会被 0 分割
对于数组内连续且相同的数据,操作区间只要包含其中一个数据,那么剩余的连续且相同的数据都可以一起包含进去,所以相当于只是一个数据,可以化简 target 数组
于是模拟每一次操作,每次操作的区间为子数组被 0 分割的部分,直到数组内所有数字都是 0 即可
对于区间内操作,利用差分数组实现
由于每一次模拟操作都需要遍历数组找到被 0 分割的区间,所以时间复杂度是 n^2
代码如下↓
class Solution {
public:int minNumberOperations(vector<int>& target) {int res = 0;int n = target.size();vector<int> target_simple; // 存储化简后的差分数组int back = 0;// 化简数组:合并连续相同值(因相同值可被同一操作覆盖)for (int i = 0; i < n; i++) {if (i && target[i] == back) continue; // 跳过连续相同值target_simple.push_back(target[i] - back); // 计算差分值back = target[i];}n = target_simple.size();int f = 1; // 循环标志:数组中是否仍有非零元素while (f) {int sum = 0; // 当前累积和(用于定位0分割点)int l = 0; // 当前区间左边界int min_v = 0x3f3f3f3f; // 当前区间内最小值for (int i = 0; i < n; i++) {sum += target_simple[i]; // 更新累积和// 当累积和归零时,说明遇到分割点if (!sum) {if (min_v == 0x3f3f3f3f) min_v = 0; // 处理空区间res += min_v; // 累加操作次数// 更新差分数组:区间左减右加target_simple[l] -= min_v;target_simple[i] += min_v;l = i + 1; // 移动左边界min_v = 0x3f3f3f3f; // 重置最小值} else {min_v = min(sum, min_v); // 更新区间最小值}}// 处理末尾未闭合区间if (l < n) {res += min_v;target_simple[l] -= min_v;}// 检查数组是否全零f = 0;sum = 0;for (int i = 0; i < n; i++) {sum += target_simple[i];if (sum) {f = 1; // 存在非零元素则继续循环break;}}}return res;}
};
方法二:差分思想
可以利用差分数组进行理解
对原数组区间进行 -1,即对原数组的差分数组的两个数进行 -1(左侧的数) 和 +1(右侧的数)
或者是只对一个数 -1(区间包含最右侧)
将原数组变为全 0,即将差分数组全变为 0
为了得到最少操作数,最好先左侧正数 -1 右侧负数 +1
由于 target 所有的数都 >0
所以差分数组中 任意负数的左侧数字之和 都 >= abs(负数)
那么在 左侧正数 -1 右侧负数 +1时,必然是右侧负数先变为 0(或同时)
此时还需要进行 只对一个数 -1 操作,才能将正数变为 0
所以决定操作次数的是正数
总操作数就是差分数组中的正数之和
即原数组中 target[0] + 所有上升沿上升的大小
或者是我自己想出来的理解
在方法一中,注意到限制操作区间的是:操作过程中会有数据变为 0 从而分割区间
相较于一步步模拟直到出现 0 ,可以直接看出哪一个数据会先变为 0
如 [4,2,3,1,2,1,5]
先讲最左侧的数字变为 0 ,需要四次操作
将 4 变成 0 的操作中,4需要操作4次,而相邻右边的 2 只能操作两次之后就变成 0 了,从而分割了操作区间,也就是说这个 2 只能连接两次区间,也可以理解为只能向右传递两次操作
第三个数字 3 只能在数字 4 变成 0 的四次操作中传递到两次,即只能 -2,所以在四次操作之后为了将 3 变为 0 我们还需要额外进行一次操作
通过上述过程,注意到每一个数字本身的大小就代表着其能够传递的操作次数
如果其相邻右侧的数字小于(等于)自身,则是相邻右侧数字先(同时)变为 0 ,不需要额外操作次数
如果大于自身,则自身传递完操作之后相邻右侧数字还需要进行额外的操作才能变为 0 ,额外的操作次数为两数之差
也就是说,如果遇到了上升沿,就需要额外的 两数之差 操作次数
总操作数为先将最左边的数变为 0 ,即 target[0] 次,额外的操作数就是相邻两上升的数字之间的差值
问:为什么一定要是相邻的两个数字
答:因为每一个数字都有不同的传递次数,只能作用于相邻的数字
[4,2,3,1,2,1,5] 中的第二个 2 只能传递 2 次,并不意味着其右边所有的数都只能操作两次,因为其相邻右边的 3 可以传递 3次操作
代码如下↓
class Solution {
public:int minNumberOperations(vector<int>& target) {int n = target.size();int res = target[0]; // 初始化:首元素操作次数for (int i = 1; i < n; i++) {if (target[i] > target[i-1]) {// 累加上升沿的增量(差分正数项)res += target[i] - target[i-1];}}return res;}
};
