1. 题目链接
2. 题目描述
给你两个整数数组 arr1
和 arr2
,返回使 arr1
严格递增所需要的最小「操作」数(可能为 0)。
每一步「操作」中,你可以分别从 arr1
和 arr2
中各选出一个索引,分别为 i
和 j
,0 <= i < arr1.length
和 0 <= j < arr2.length
,然后进行赋值运算 arr1[i] = arr2[j]
。
如果无法让 arr1
严格递增,请返回 -1
。
3. 题目示例
示例 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 严格递增。
4. 解题思路
- 问题理解:该算法解决的是如何通过最少的替换操作(将a中的元素替换为b中的元素)使得数组a严格递增。
- 核心思想:使用动态规划+记忆化搜索的方法:
- 对数组b进行排序以便二分查找
- 从数组a的末尾开始向前处理
- 对于每个元素,考虑两种情况:替换或不替换
- 使用记忆化存储中间结果避免重复计算
- 关键步骤:
- 排序数组b以便快速查找合适的替换值
- 递归处理每个元素,比较替换和不替换的情况
- 使用二分查找快速定位b中小于当前前驱值的最大数
- 记忆化存储计算结果优化性能
- 边界处理:
- 初始时假设最后一个元素后面有一个无穷大的数
- 使用Integer.MAX_VALUE/2作为无法完成操作的标志
5. 题解代码
class Solution {
private int[] a, b; // 原始数组a和备用数组b
private Map<Integer, Integer> memo[]; // 记忆化存储,用于避免重复计算
public int makeArrayIncreasing(int[] a, int[] b) {
this.a = a;
this.b = b;
Arrays.sort(b); // 为能二分查找,对b排序
int n = a.length;
memo = new HashMap[n]; // 初始化记忆化数组
Arrays.setAll(memo, e -> new HashMap<>());
// 从最后一个元素开始处理,假设a[n-1]右侧有个无穷大的数
int ans = dfs(n - 1, Integer.MAX_VALUE);
// 如果结果过大说明无法完成,返回-1
return ans < Integer.MAX_VALUE / 2 ? ans : -1;
}
private int dfs(int i, int pre) {
if (i < 0) return 0; // 基本情况:处理完所有元素
if (memo[i].containsKey(pre))
return memo[i].get(pre); // 如果之前计算过,直接返回结果
// 不替换a[i]的情况
int res = a[i] < pre ? dfs(i - 1, a[i]) : Integer.MAX_VALUE / 2;
// 二分查找b中小于pre的最大数的下标
int k = lowerBound(b, pre) - 1;
if (k >= 0) // 如果找到合适的替换数
res = Math.min(res, dfs(i - 1, b[k]) + 1); // 比较替换和不替换的最小操作数
memo[i].put(pre, res); // 存储计算结果
return res;
}
// 二分查找辅助函数,找到第一个大于等于target的数的位置
private int lowerBound(int[] nums, int target) {
int left = -1, right = nums.length; // 开区间(left, right)
while (left + 1 < right) { // 区间不为空
int mid = (left + right) >>> 1; // 无符号右移防止溢出
if (nums[mid] < target)
left = mid; // 范围缩小到(mid, right)
else
right = mid; // 范围缩小到(left, mid)
}
return right;
}
}
6. 复杂度分析
时间复杂度:O(n_m_logm),其中n是数组a的长度,m是数组b的长度。这是因为:
- 排序b数组需要O(m logm)时间
- 记忆化搜索中,每个状态(i, pre)最多被计算一次,其中i有n种可能,pre最多有m种可能(因为pre只能是b中的某个值或a中的原始值)
- 对于每个状态,我们需要执行一次二分查找,耗时O(logm)
空间复杂度:O(n*m),主要用于存储记忆化结果。记忆化数组memo的大小为n,每个元素是一个哈希表,在最坏情况下可能存储m个键值对。