1187. 使数组严格递增
难度:中等
题目:
给你两个整数数组 arr1
和 arr2
,返回使 arr1
严格递增所需要的最小「操作」数(可能为 0)。
每一步「操作」中,你可以分别从 arr1
和 arr2
中各选出一个索引,分别为 i
和 j
,0 <= i < arr1.length
和 0 <= j < arr2.length
,然后进行赋值运算 arr1[i] = arr2[j]
。
如果无法让 arr1
严格递增,请返回 -1
。
示例 1:
输入: arr1 = [1,5,3,6,7], arr2 = [1,3,2,4]
输出: 1
解释: 用 2 来替换 5,之后 arr1 = [1, 2, 3, 6, 7]。
示例 2:
输入: arr1 = [1,5,3,6,7], arr2 = [4,3,1]
输出: 2
解释: 用 3 来替换 5,然后用 4 来替换 3,得到 arr1 = [1, 3, 4, 6, 7]。
示例 3:
输入: 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
个人思路
思路与算法
解法一 动态规划
此题为经典的「300. 最长递增子序列」问题的变形题目,我们可以参考类似的题目解法。首先我们思考一下,由于要求数组严格递增,因此数组中不可能存在相同的元素,对于数组 arr2\textit{arr}_2arr 2 来说,可以不需要考虑数组中的重复元素,可以预处理去除 arr2\textit{arr}_2arr 2 的重复元素,假设数组 arr1\textit{arr}_1arr 1 的长度为 nnn,数组 arr2\textit{arr}_2arr 2 的长度为 mmm,此时可以知道最多可以替换的次数为 min(n,m)\min(n,m)min(n,m)。如何才能定义动态规划的递推公式,这就需要进行思考。我们设 dp[i][j]\textit{dp}[i][j]dp[i][j] 表示数组 arr1\textit{arr}_1arr 1 中的前 iii 个元素进行了 jjj 次替换后组成严格递增子数组末尾元素的最小值。当我们遍历 arr1\textit{arr}_1arr 1 的第 iii 个元素时,此时 arr1[i]\textit{arr}_1[i]arr 1 [i] 要么进行替换,要么进行保留,实际可以分类进行讨论:
此时如果 arr1[i]\textit{arr}_1[i]arr 1 [i] 需要进行保留,则 arr1[i]\textit{arr}_1[i]arr 1 [i] 一定严格大于前 i−1i-1i−1 个元素替换后组成的严格递增子数组最末尾的元素。假设前 i−1i-1i−1 个元素经过了 jjj 次变换后得到的递增子数组的末尾元素的最小值为 dp[i−1][j]\textit{dp}[i-1][j]dp[i−1][j],如果满足 arr1[i]>dp[i−1][j]\textit{arr}_1[i] > \textit{dp}[i-1][j]arr 1 [i]>dp[i−1][j],则此时 arr1[i]\textit{arr}_1[i]arr 1 [i] 可以保留加入到该子数组中且构成的数组严格递增; 此时如果 arr1[i]\textit{arr}_1[i]arr 1 [i] 需要进行替换,则替换后的元素一定严格大于前 i−1i-1i−1 个元素替换后组成的严格递增子数组最末尾的元素。假设前 i−1i-1i−1 个元素经过了 j−1j-1j−1 次变换后得到的递增子数组的末尾元素的最小值为 dp[i−1][j−1]\textit{dp}[i-1][j-1]dp[i−1][j−1],此时我们从 arr2\textit{arr}_2arr 2 找到严格大于 dp[i−1][j−1]\textit{dp}[i-1][j-1]dp[i−1][j−1] 的最小元素 arr2[k]\textit{arr}_2[k]arr 2 [k],则此时将 arr2[k]\textit{arr}_2[k]arr 2 [k] 加入到该子数组中且构成数组严格递增; 综上可知,每个元素在替换时只有两种选择,要么选择保留当前元素 arr1arr_1arr 1 ,要么从 arr2arr_2arr 2 中选择一个满足条件的最小元素加入到数组中,最少替换方案一定包含在上述替换方法中。我们可以得到以下递推关系: {dp[i][j]=min(dp[i][j],arr1[i]),if arr1[i]>dp[i−1][j]dp[i][j]=min(dp[i][j],arr2[k]),if arr2[k]>dp[i−1][j−1] \begin{cases} \textit{dp}[i][j] = \min(\textit{dp}[i][j],\textit{arr}_1[i]), \quad & \textbf{if} \ \textit{arr}_1[i] > \textit{dp}[i-1][j] \ \textit{dp}[i][j] = \min(\textit{dp}[i][j],\textit{arr}_2[k]), \quad & \textbf{if} \ \textit{arr}_2[k] > \textit{dp}[i-1][j-1] \end{cases} { dp[i][j]=min(dp[i][j],arr 1 [i]), dp[i][j]=min(dp[i][j],arr 2 [k]),
if arr 1 [i]>dp[i−1][j] if arr 2 [k]>dp[i−1][j−1]
为了便于计算,我们将 dp[i][j]\textit{dp}[i][j]dp[i][j] 的初始值都设为 ∞\infty∞,为了便于计算在最开始加一个哨兵,此时令 dp[0][0]=−1\textit{dp}[0][0] = -1dp[0][0]=−1 表示最小值。实际计算过程如下:
为了方便计算,需要对 arr2\textit{arr}_2arr 2 进行预处理,去掉其中的重复元素,为了快速找到数组 arr2\textit{arr}_2arr 2 中的最小元素,还需要对 arr2\textit{arr}_2arr 2 进行排序; 依次尝试计算前 iii 个元素在满足 jjj 次替换时的最小元素: 如果当前元素 arr1[i]\textit{arr}_1[i]arr 1 [i] 大于 dp[i][j−1]\textit{dp}[i][j-1]dp[i][j−1],此时可以尝试将 arr1[i]\textit{arr}_1[i]arr 1 [i] 替换为 dp[i][j]\textit{dp}[i][j]dp[i][j],即此时 dp[i][j]=min(dp[i][j],arr1[i])\textit{dp}[i][j] = \min(\textit{dp}[i][j],\textit{arr}_1[i])dp[i][j]=min(dp[i][j],arr 1 [i])。 如果前 i−1i-1i−1 个元素可以满足 j−1j-1j−1 次替换后成为严格递增数组,即满足 dp[i−1][j−1]≠∞\textit{dp}[i-1][j-1] \neq \inftydp[i−1][j−1] =∞,可以尝试在第 jjj 次替换掉 arr1[i]\textit{arr}_1[i]arr 1 [i],此时根据贪心原则,利用二分查找可以快速的找到严格大于 dp[i−1][j−1]\textit{dp}[i-1][j-1]dp[i−1][j−1] 的最小值进行替换即可。 设当前数组 arr1[i]\textit{arr}_1[i]arr 1 [i] 的长度为 nnn,如果前 nnn 个元素满足 jjj 次替换后成为严格递增数组,此时我们找到最小的 jjj 返回即可。
代码
class Solution {
public:
int maxSumAfterPartitioning(vector<int> &arr, int k) {
function<int(int)> dfs = [&](int i) -> int {
// i=-1 时不会进入循环
int res = 0;
for (int j = i, mx = 0; j > i - k && j >= 0; --j) {
mx = max(mx, arr[j]); // 一边枚举 j,一边计算子数组的最大值
res = max(res, dfs(j - 1) + (i - j + 1) * mx);
}
return res;
};
return dfs(arr.size() - 1);
}
};
解法二 递归 + 记录返回值 = 记忆化搜索
上面的做法太慢了,怎么优化呢?
举个例子,「先分隔出 arr[n−1],再分隔出 arr[n−2](两个子数组)」和「分隔出 arr[n−2] 到 arr[n−1](一个子数组)」,都会递归到 dfs(n−3)。
一叶知秋,整个递归中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo 数组(或哈希表)中。 如果一个状态不是第一次遇到,那么直接返回 memo 中保存的结果。
class Solution {
public:
int maxSumAfterPartitioning(vector<int> &arr, int k) {
int n = arr.size(), memo[n];
memset(memo, -1, sizeof(memo)); // -1 表示还没有计算过
function<int(int)> dfs = [&](int i) -> int {
if (i < 0) return 0;
int &res = memo[i]; // 注意这里是引用,下面会直接修改 memo[i]
if (res != -1) return res; // 之前计算过了
for (int j = i, mx = 0; j > i - k && j >= 0; --j) {
mx = max(mx, arr[j]); // 一边枚举 j,一边计算子数组的最大值
res = max(res, dfs(j - 1) + (i - j + 1) * mx);
}
return res;
};
return dfs(n - 1);
}
};
每天记录一下做题思路。