【每日一题】只有两个键的键盘(dp、数学), LIS问题变种:最长递增子序列的个数(dp+二分 nlogn)

650 只有两个键的键盘(dp、数学)

在这里插入图片描述

一道dp的题目,但是美妙之处在于这道题有数学解法。
首先对于dp的思路,dp[i]表示 i 个字符时候的最小次数,dp[i] = dp[i/j]+j 得到转移方程。一个小优化在于,因为是找的因子,i/j * j = i 因此我们枚举到 i \sqrt{i} i 就可以了。

更为巧妙的数学解法是,进行拆分的时候肯定 a1*a2*a3 = n 一定是要找到因子的。而实际的代价是 a1+a2+a3 考虑到a1*a2>=a1+a2。因此,我们其实找到全部质因子就可以

class Solution {
    public int minSteps(int n) {
       int ans = 0;
       // 分解质因数,但是这里其实不是最佳的。
       for(int i = 2;i*i<=n;i++){
           while(n%i ==0){
               n /= i;
               ans += i;
           }
       }
       // 最后剩余了一个质数
       if(n>1) ans += n;
       return ans;
       =================================================
        int[] dp = new int[n+1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[1] = 0;
        for (int i = 2;i<=n;i++){
            // 这里其实可以找到两个因子 j和i/j; 
            for(int j = 1;j*j<=i;j++){
                if (i%j == 0){
                    dp[i] = Math.min(dp[i], dp[j]+i/j);
                    dp[i] = Math.min(dp[i], dp[i/j]+j);
                }
            }
        }
        return dp[n];
    }
}

LIS问题变种:最长递增子序列的个数(dp+二分 nlogn)

在这里插入图片描述

LIS问题是非常经典的问题了,如果要求解寻找最长递增子序列的长度,可以nlogn,这个不难。
这个题目是升级版本,要求得到最长子序列的个数。需要更加复杂的dp+二分了。

当然我们可以使用简单的 n 2 n^2 n2的方法,维护一个cnt[i]表示使用 i 得到最长序列的种类数,dp[i]表示以 i 结尾的最长序列。

class Solution {
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length, maxLen = 0, ans = 0;
        int[] dp = new int[n];
        int[] cnt = new int[n];
        for (int i = 0; i < n; ++i) {
            dp[i] = 1;
            cnt[i] = 1;
            for (int j = 0; j < i; ++j) {
                if (nums[i] > nums[j]) {
                    if (dp[j] + 1 > dp[i]) {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j]; // 重置计数
                    } else if (dp[j] + 1 == dp[i]) {
                        cnt[i] += cnt[j];
                    }
                }
            }
            if (dp[i] > maxLen) {
                maxLen = dp[i];
                ans = cnt[i]; // 重置计数
            } else if (dp[i] == maxLen) {
                ans += cnt[i];
            }
        }
        return ans;
    }
}

更优的,我们考虑一个nlogn的方法 dp.get(i).get(j) 表示长度为 i 的第 j 种可能。 这里需要注意,如果是从前往后遍历的话,其实很容易发现,这个dp.get(i)一定是单调不增的,否则一定可以增加长度。

例子 我们可以假想插入 4.55
 [1,3,5,4,7]
 1/ 1
 2/ 3
 3/ 5 4
 4/ 7

cnt[i][j] 是前缀和的思想,表示长度为 i 的时候,前 j 个元素作为结尾的全部可能数量。当我们遍历到 nums 的某个数字的时候,查看dp每个list的最后一个数字(因为是最小的)。如果大于,说明可以这个list里面起码有小于的当前数字的。假设是k。

然后我们可以二分的找到 dp.get(k) 种的边界 p (第一个小于 nums[i] 的数字),在 p 之后的数字都是小于 numus[i] 的。因此 nums[i] 和这些数字可以组成长度为k+1的序列,而这些数字的可能数量是 newadd = cnt.get(k).get(size()-1) - cnt.get(k).get(p)

最后在dp.get(k+1).add(nums[i]) ; cnt.get(k+1).add(last+1)

class Solution {
    public int findNumberOfLIS(int[] nums) {
        // 如果是n2的方法,就是暴力dp
        List<List<Integer>> dp = new ArrayList<>();
        List<List<Integer>> cnt = new ArrayList<>();
        int n = nums.length;
        for(int i = 0;i<n;i++){
            int k = findList(dp, nums[i]);
            int newadd = 1;
            if (k >= 0){
                int p = findInList(dp.get(k), nums[i]);
                int size = cnt.get(k).size();
                newadd = cnt.get(k).get(size-1)-cnt.get(k).get(p);
            }
            if(k == dp.size()-1){
                dp.add(new ArrayList<Integer>());
                cnt.add(new ArrayList<Integer>());
                cnt.get(k+1).add(0);
                
                dp.get(k+1).add(nums[i]);
                cnt.get(k+1).add(newadd);
            }else{
                dp.get(k+1).add(nums[i]);
                int cntSize = cnt.get(k+1).size();
                cnt.get(k+1).add(cnt.get(k+1).get(cntSize-1)+newadd);
            }
        }
        int size = cnt.size();
        int lastszie = cnt.get(size-1).size();
        return cnt.get(size-1).get(lastszie-1);
    }
    // 返回的是最后一个元素一定小于的,且再大就一定大于等于的。
    public int findList(List<List<Integer>> dp, int target){
        int n = dp.size();
        int l = 0; 
        int r = n-1;
        while(l<=r){
            int mid = l+(r-l)/2;
            int size = dp.get(mid).size();
            int cur = dp.get(mid).get(size-1);
            if (cur>target){
                r = mid-1;
            }else if (cur < target){
                l = mid+1;
            }else{
                return mid-1;
            }
        }
        return r;
    }
    // 返回小于的第一个
    public int findInList(List<Integer> nums, int target){
        int n = nums.size();
        int l = 0;
        int r = n-1;
        while(l<=r){
            int mid = l+(r-l)/2;
            int cur = nums.get(mid);
            if (cur>target){
                l = mid+1;
            }else if (cur < target){
                r = mid-1;
            }else{
                l = mid+1;
            }
        }
        return l;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值