LC-1187. 使数组严格递增(记忆化搜索==>动态规划)

1187. 使数组严格递增

难度困难107

给你两个整数数组 arr1arr2,返回使 arr1 严格递增所需要的最小「操作」数(可能为 0)。

每一步「操作」中,你可以分别从 arr1arr2 中各选出一个索引,分别为 ij0 <= i < arr1.length0 <= 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.cn/problems/make-array-strictly-increasing/solution/zui-chang-di-zeng-zi-xu-lie-de-bian-xing-jhgg/

为方便描述,下文将 arr1 简记为 a ,arr2 简记为 b。问题等价于从 a 中找到一个最长严格递增子序列 lis,使得把不在 lis 中的元素替换成b中的元素后,a 是严格递增的,求不在 lis 中的元素个数的最小值

对于最长递增子序列问题 (或者一般的动态规划问题),通常都可以用[选或不选]和[枚举选哪个]来启发思考。

方法一:选还是不选

例如 a=[0,4,2,2],b=[1,2,3,4],假如 a[3] =2 换成了b[3] = 4,那么对于 a[2] =2 来说

  • 选择不替换,问题变成[把[0,4] 替换成严格递增数组,且数组的最后一个数小于 2,所需要的最小操作数]。

  • 选择替换,那么应该替换得越大越好,但必须小于 4 (因为a[3] 替换成了4),那么换成3最佳,问题变成[把[0,4]替换成严格递增数组,且最后一个数小于 3,所需要的最小操作数].

    把从 a[0] 到 a[1] 的这段前缀替换成严格递增数组,且数组的最后一个数小于 pre,所需要的最小操作数。记为 dfs(i,pre)

记忆化搜索

class Solution {
    int[] a, b;
    Map<Integer, Integer> memo[];
    public int makeArrayIncreasing(int[] arr1, int[] arr2) {
        this.a = arr1;
        this.b = arr2;
        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/2);
        return ans < Integer.MAX_VALUE/3 ? ans : -1;
    }

    /**
    例如 a=[0,4,2,2],b=[1,2,3,4],假如 a[3] =2 换成了b[3] = 4,那么对于 a[2] =2 来说
        选择不替换,问题变成[把[0,4] 替换成严格递增数组,且数组的最后一个数小于 2,所需要的最小操作数]。
        选择替换,那么应该替换得越大越好,但必须小于 4 (因为a[3] 替换成了4),那么换成3最佳,问题变成[把[0,4]替换成严格递增数组,且最后一个数小于 3,所需要的最小操作数]。
     */
    // 把从 a[0] 到 a[1] 的这段前缀替换成严格递增数组,且数组的最后一个数小于 pre,所需要的最小操作数。记为 dfs(i,pre)
    public int dfs(int i, int pre){
        if(i < 0) return 0;
        if(memo[i].containsKey(pre)) return memo[i].get(pre);
        // 不替换
        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) { // a[i]替换成小于 pre 的最大数
            res = Math.min(res, dfs(i-1, b[k]) + 1);
        }
        memo[i].put(pre, res);
        return res;
    }   
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int target){
        int left = 0, right = nums.length;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

方法二:枚举选哪个

在方法二中,我们把重点放在 s 上,关注哪些 a[i] 没有被替换,那么答案就是 n -length(lis)

记忆化搜索

仿照最长递增子序列的状态定义,用 dfs(i) 表示以 a[i] 结尾的 lis 的长度,这里a[i] 没有被替换

  • 枚举a[i] 左侧最近的没有被替换的元素a[j],那么必须满足从 a[j+1]a[i-1]的这段数组,能被替换成b中的元素,且替换后从a[j]a[i] 是严格递增的。为了保证替换的元素互不相同,需要对 b去重。

b[k]>=a[i] 的最小元素,注: 即使 b[k] 不存在也没关系,下面不会用到这个数

a[i -1] 最大可以替换成 b 中小于 a[i] 的最大元素,即 b[k - 1],然后a[i - 2] 最大可以替换成 b[k - 2],…,a[j +1] 最大可以替换成b[k-(i-j-1)]

注: 从a[j+1]a[i-1]一共有i-j-1个数。
所以,只有满足 b[k -(i -j- 1)] >a[j],才能完成替换操作。此时更新 dfs(i) = max(dfs(i), dfs(j) + 1)
注: 要求k-(i-j-1)>= 0,也就是j>=i-k -1

class Solution {
    int[] a, b, memo;
    int m;
    public int makeArrayIncreasing(int[] arr1, int[] arr2) {
        this.a = arr1;
        this.b = arr2;
        Arrays.sort(b);// 为了能二分查找,对b排序
        for(int i = 1; i < b.length; i++){
            if(b[m] != b[i]){
                b[++m] = b[i]; // 原地去重
            }
        }
        ++m;
        int n = a.length;
        memo = new int[n+1]; // 0 表示还没有计算过
        int ans = dfs(n);
        return ans < 0 ? -1 : n + 1 - ans;
    }

    public int dfs(int i){
        if(memo[i] != 0) return memo[i];
        int x = i < a.length ? a[i] : Integer.MAX_VALUE;
        int k = lowerbound(b, m, x);
        int res = k < i ? Integer.MIN_VALUE : 0; // 小于 a[i] 的数全部替换
        if(i > 0 && a[i-1] < x) { // 无替换
            res = Math.max(res, dfs(i-1));
        } 
        for(int j = i-2; j > i-k-1 && j >= 0; j--){
            if (b[k - (i - j - 1)] > a[j])
                // a[j+1] 到 a[i-1] 替换成 b[k-(i-j-1)] 到 b[k-1]
                res = Math.max(res, dfs(j));
        }
        return memo[i] = ++res; // 把 +1 移到这里,表示 a[i] 不变
    }

    
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int right, int target){
        int left = 0;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

翻译成递推

class Solution {
    public int makeArrayIncreasing(int[] a, int[] b) {
        int n = a.length, m = 0;
        Arrays.sort(b);// 为了能二分查找,对b排序
        for(int i = 1; i < b.length; i++){
            if(b[m] != b[i]){
                b[++m] = b[i]; // 原地去重
            }
        }
        ++m;
        var f = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            int x = i < n ? a[i] : Integer.MAX_VALUE;
            int k = lowerbound(b, m, x);
            int res = k < i ? Integer.MIN_VALUE : 0; // 小于 a[i] 的数全部替换
            if (i > 0 && a[i - 1] < x) // 无替换
                res = Math.max(res, f[i - 1]);
            for (int j = i - 2; j > i - k - 1 && j >= 0; --j)
                if (b[k - (i - j - 1)] > a[j])
                    // a[j+1] 到 a[i-1] 替换成 b[k-(i-j-1)] 到 b[k-1]
                    res = Math.max(res, f[j]);
            f[i] = res + 1; // 把 +1 移到这里,表示 a[i] 不替换
        }
        return f[n] < 0 ? -1 : n + 1 - f[n];    
    }
  
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int right, int target){
        int left = 0;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值