【每日一题】使数组严格递增
1187. 使数组严格递增
关键词:动态规划
题目来源:1187. 使数组严格递增 - 力扣(Leetcode)
题目描述
T动态规划
给你两个整数数组 arr1 和 arr2,返回使 arr1 严格递增所需要的最小「操作」数(可能为 0)。
每一步「操作」中,你可以分别从 arr1 和 arr2 中各选出一个索引,分别为 i 和 j,0 <= i < arr1.length 和 0 <= j < arr2.length,然后进行赋值运算 arr1[i] = arr2[j]。
如果无法让 arr1 严格递增,请返回 -1。
输入:arr1 = [1,5,3,6,7], arr2 = [1,3,2,4]
输出:1
输入:arr1 = [1,5,3,6,7], arr2 = [4,3,1]
输出:2
输入:arr1 = [1,5,3,6,7], arr2 = [1,6,3,3]
输出:-1
解释:无法使 arr1 严格递增。
数据范围
1 <= arr1.length, arr2.length <= 2000
0 <= arr1[i], arr2[i] <= 10^9
朴素求解
(后续讨论,数组下标均从1开始,代码中数组下标依题目从0开始,注意转换)
先考虑能不能构成递增数组。
对于每一个数都需要考虑要不要替换,假设当前考虑到a[i]
-
若保留a[i],则a[i]至少要大于前i-1个数构成的递增数组的末尾值,否则不能保留
前i-1个数构成的递增数组可能有多个,对应的末尾值也有多个,为了尽量保留a[i],取最小的末尾值进行比较即可。
-
若替换a[i],则用于替换的元素必须大于前i-1个数构成的递增数组的末尾值,不存在这样的元素则不能替换
前i-1个数构成的递增数组可能有多个,对应的末尾值也有多个,为了尽可能的使得后面好构成递增数组,当前用于替换的元素应该尽可能小,故取最小的末尾值进行比较即可。
始终记住,我们只需要尽可能地凑出一个答案即可,所以上述的末尾值均取最小,从而使得后续尽可能地构造递增数组。
上述两种情况都用到“最小末尾值”,于是“记录末尾最小值”,设f[i]=前i个元素构成的所有递增数组中的末尾值的最小值。当前i个数(经过多次替换)不能构成递增数组时,f[i]即为INF。
若保留a[i],则第i个位置仍为a[i],若替换a[i],则第i个位置为替换后的元素,f(i)取二者的较小值。
再考虑最小操作次数。
由于f[i]的值已经被定义为“最小末尾值”,因此需要增加一维用于记录操作次数,对数组f修改如下
设f(i,j)=前i个数,经过j次替换后,得到的递增数组的末尾元素的最小值,对于a[i]
- 若保留a[i],则a[i]必须严格大于f(i-1, j),则f(i,j)=a[i]
- 若替换a[i],则替换后的元素必须严格大于f(i-1, j),且尽可能小,则f(i,j)为替换后的元素
同样,若前i个数无法通过j次替换得到严格递增数组,则将f(i,j)标记为INF。于是,第一个不等于INF的f(n, j)即为最终答案。
int makeArrayIncreasing(vector<int> &arr1, vector<int> &arr2) {
int INF = 0x3f3f3f3f;
// 预处理arr2:排序去重
sort(arr2.begin(), arr2.end());
arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end());
int n = arr1.size(), m = arr2.size();
int f[n + 1][min(n, m) + 1];
memset(f, 0x3f, sizeof f);
f[0][0] = -1; // 考虑前0个数替换0次得到的递增数组的末尾的最小值记为-1
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= min(i, m); j++) {
// 保留
if (arr1[i - 1] > f[i - 1][j])
f[i][j] = arr1[i - 1];
// 替换
// 需保证前i-1个元素替换j-1次能得到递增数组,否则替换arr1[i]无意义
if (j > 0 && f[i - 1][j - 1] != INF) {
// 找到大于f[i-1][j-1]的最小值
// 既然现在是第j次替换,说明前面有j-1次替换,也即至少arr2中的前j-1个数是不用考虑的
auto it = upper_bound(arr2.begin() + j - 1, arr2.end(), f[i - 1][j - 1]);
if (it != arr2.end())f[i][j] = min(*it, f[i][j]);
}
if (f[n][j] != INF)return j; // 取最小操作数
}
}
return -1;
}
时间复杂度:O( n( logm+min(m,n) ) )
空间复杂度:O( n×min(m,n) )
维度压缩
在朴素求解中,为了确定第i个位置上的数,需要参照前i-1个数构成的递增数组中的最小末尾值,从而需要增加一维变量(f的值被占用,增加一维记录操作次数)。现在,不妨固定第i+1个数来确定第i个位置上的数,也即固定第i个位置上的数来确定第i-1个位置上的数。于是,可用f的值用来记录最小操作次数。
也即,设f[i]=a[i]不动,使得前i项为递增数组的最小操作次数。
固定a[i],考虑a[i-1]
- 若保留a[i-1],则需要满足a[i-1]<a[i],f[i]=f[i-1]
- 若替换a[i-1],则设上一个保留的位置为j,a[j+1...i-1]这些位置是要被填充的,既然这个区间的填充次数已经确定,那么填充什么值就无所谓了,只需要满足大于a[j]小于a[i]且不重复即可,f[i]=f[j]+(i-j-1)
由于最后一个元素也可能被替换,因此,还需要在数组a后面追加一个一定不会被替换的元素,不妨定为INF。
f[n]即为最终的答案,n为插入INF后的数组长度
如何找到(i-j-1)个大于a[j]小于a[i]的元素呢?两种方案:
- 找到第一个大于a[j]的元素,往后数到第(i-j-1)个,看第(i-j-1)个是否小于a[i]。
- 找到第一个小于a[i]的元素,往前数到第(i-j-1)个,看第(i-j-1)个是否大于a[j]。
由于上一个保留的位置是不确定的,所以需要枚举上一个保留的位置。因为a[i]是固定的,所以采用第二种方案。
由于并不是每个位置都可以作为上一个被保留的位置,也即可能找不到(i-j-1)个符合条件的数,所以,为了方便,不妨枚举替换的元素个数,枚举个数取min(i-1, k),其中k为小于a[i]的元素个数。
int makeArrayIncreasing(vector<int> &arr1, vector<int> &arr2) {
int INF = 0x3f3f3f3f;
// 预处理:排序、去重、末尾追加INF
sort(arr2.begin(), arr2.end());
arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end());
arr1.push_back(INF);
int n = arr1.size(), f[n];
memset(f, 0x3f, sizeof f);
f[0] = 0; // 考虑前1个数
for (int i = 1; i < n; i++) {
// 保留arr1[i-1]
if (arr1[i - 1] < arr1[i])f[i] = f[i - 1];
// 不保留arr1[i-1]
int k = lower_bound(arr2.begin(), arr2.end(), arr1[i]) - arr2.begin();
for (int j = min(i, k); j; j--) {
// 最多可以替换min(i,k)个数
if (i == j) f[i] = min(f[i], j); // 防止越界
else if (arr1[i - j - 1] < arr2[k - j])
f[i] = min(f[i], f[i - j - 1] + j);
}
}
return f[n - 1] >= INF ? -1 : f[n - 1];
}
时间复杂度:O( n( logm+min(m,n) ) )
空间复杂度:O(n)

浙公网安备 33010602011771号