LEETCODE 1187. Make Array Strictly Increasing 解题思路分析

题目大意:

使数组严格递增

给你两个整数数组 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

如果想查看本题目是哪家公司的面试题,请参考以下免费链接: https://leetcode.jp/problemdetail.php?id=1187

解题思路分析:

这是一道Hard难度的题目。难度在于我们不知道当前位置的数字是否该被替换。遇到这种题目我们就应该想到遍历所有的可能,即当前位置可以出现哪些种情况,然后再在这些分支中找到最优解。

由于题目要求数组需满足完全递增排序,因此,若当前数字小于等于前面的数字,那么一定需要替换,反之,则需要考虑替换或者不替换两种情况。如果不做替换的情况下,操作次数不会增加,当前位的操作次数就等于上一位的操作次数。若是发生替换,操作次数应该加一。接下来的关键就是找出当前数字可以替换为哪些数字?

理论上,在arr2数组中所有大于前一位的数字都是可以被选择的目标,考虑到之后的递增排序,这个目标值肯定越小越好,因此我们需要找到一个大于前一位数字的最小数作为替换目标。举个例子来说明,比如:

arr1 = [1,5,3,4]
arr2 = [2,3,6]

假设当前位于arr1下标为1的位置,即数字5,因为5大于前一位的1,进而5本身是一个选择,此外,在arr2中大于前一位1的所有数字中的最小值2也是一个备选方案。这时,下标为1的选择有5和2两种情况,保持5不变需要的操作次数为0,选择2的操作次数为1。即:

// index为1时有2种选择
5 -> 0 // 选择5需要0次操作
2 -> 1 // 选择1需要1次操作

继续循环到下标为2的位置,即数字3。由于前一位有2种可能,如果前一位选择5的话,当前位的3大于它所以必须被替换掉,在arr2中大于5的最小值为6,因此6为一种方案,操作次数加一。另外一种情况,如果前一位选择2的话,当前位3符合条件,可以无需替换,操作次数不变仍为1(因为前一位选择2时已经执行了一次操作)。最后,我们还可以选择比前一位2大的最小数3作为一种替换方案,此时操作次数加一,应该为2。即:

// index为2时有3种选择
6 -> 1 // 前一位为5,选择6替换,当前1次操作,总共1次操作
3 -> 1 // 前一位为2,保持当前位,当前0次操作,总共1次操作
3 -> 2 // 前一位为2,选择3替换,当前1次操作,总共2次操作

接下来的循环同理,最后我们只要找到一个操作次数最少的值即是答案。

编码时,我们需要记录下当前位作出的选择,以及执行该选择需要执行操作的总次数。循环结束时找到最优解即可。

基本原理不难理解,但是有一点需要优化,否则执行效率会很低。比如前一位有N种选择,这N种选择的数字都小于当前位,那么当前位可以保持不变,操作次数也与前位相同,不过问题来了,这N种情况各自的操作次数不一定一致,我们应该选择哪一个?按照最优理论,答案肯定要选最少的那个。

看下完整代码:

public int makeArrayIncreasing(int[] arr1, int[] arr2) {
    Arrays.sort(arr2); // 方便查找,将arr2排序
    // 存储每一步的可能性
    // int[0]为选择的数字,int[1]为该选择需要的操作次数
    Queue<int[]> q = new LinkedList<>();
    q.offer(new int[]{-1, 0}); // 初始化,假想第0位的前一位是-1。
    for(int i=0;i<arr1.length;i++){ // 循环arr1
        int size = q.size(); // 计算queue的大小
        int minCount=Integer.MAX_VALUE; // 前一位所有选择的最小次数
        while(size-->0){ // 当size变为0时,前一位的所有可能遍历完毕
            int[] n = q.poll(); // 取前一位的一个选择
            if(arr1[i] > n[0]){ // 如果当前位大于前一位。
                // 先不急将当前选择加入queue中,找到最小的再说。
                minCount = Math.min(minCount, n[1]);
            }
            // 选择一个比前一位大的最小数。
            int min = findMin(arr2, n[0]);
            if(min != -1){ // 如果找到了
                // 将这个最小数加入到queue中
                // 操作次数在前一位的基础上加一
                q.offer(new int[]{min, n[1]+1});
            }
        } // 前一位所有情况循环结束
        // 如果当前位可以保持不变,将最小次数加入到queue中
        if(minCount!=Integer.MAX_VALUE){
            q.offer(new int[]{arr1[i], minCount});
        }
    } // arr1循环结束
    // 如果最后一位没有合法的选择方案,返回-1。
    if(q.size()==0){
        return -1;
    }
    int res=Integer.MAX_VALUE;
    while(q.size()>0){ // 在queue中找到一个最小值最为返回结果
        res = Math.min(res, q.poll()[1]);
    }
    return res;
}
Map<Integer, Integer> memo = new HashMap<>();
// 寻找一个大于min的最小值
int findMin(int[] arr2, int min){
    if(memo.containsKey(min)){ // memo用于减少重复查询
        return memo.get(min);
    }
    for(int i=0;i<arr2.length;i++){
        if(arr2[i]<=min){
            continue;
        }else{
            memo.put(min, arr2[i]);
            return arr2[i];
        }
    }
    memo.put(min, -1);
    return -1;
}

如果将findMin方法改为2分查找还可以大幅提高效率。时间原因,略过。

本解法运行时间92ms。

Runtime: 92 ms, faster than 60.15% of Java online submissions for Make Array Strictly Increasing.

Memory Usage: 39.4 MB, less than 100.00% of Java online submissions for Make Array Strictly Increasing.

本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/leetcode-1187-make-array-strictly-increasing-解题思路分析/
此条目发表在leetcode分类目录,贴了, , , 标签。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。