【专题讲解】序列DP

文章目录

  • LIS
    • 最长递增子序列的长度
    • 最长递增子序列的个数
    • 354. 俄罗斯套娃信封问题
  • 编辑距离
  • 打家劫舍类
    • 740. 删除并获得点数
  • 446. 等差数列划分 II - 子序列

什么是序列DP
序列dp是一类最长,最多的子序列的相关问题。 状态转移方程中 dp[i] 表示前i个元素a [0],a [1],…a [i-1]的某种性质, 坐标型 dp 状态转移方程中f [i]表示以元素a [i]结尾的某种性质。

LIS

最长递增子序列的长度

最长递增子序列的个数

class Solution {
     
    public int findNumberOfLIS(int[] nums) {
     
        // 如果是n2的方法,就是暴力dp
        // 研究一个nlogn的方法  dp.get(i).get(j) 表示长度为i的第j种可能。
        // 这里需要注意,如果是从前往后遍历的话,其实很容易发现,这个dp.get(i)一定是单调不增的,否则一定可以增加长度
        // cnt[i][j] 是前缀和的思想,表示长度为i的时候,前j个元素作为结尾的全部可能数量
        // 当我们遍历到某个数字的时候,查看dp每个list的最后一个数字(因为是最小的),如果大于,说明可以这个list里面起码有小于的当前数字的。假设是k
        // 然后我们可以二分的找到边界p,并且用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+)
        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;
    }
}

354. 俄罗斯套娃信封问题

【专题讲解】序列DP_第1张图片

经典LIS的变种问题,我们首先需要对信封进行某种排序,比如首先排序第一维从小到大,然后第二维从大到小。这样保证后面的信封只要比较第二个维度就可以知道是不是可以放进去。(如果是第二个维度从小到大就有问题了,比如[4, 5] [4,6], 只比较5 6 其实可以,但是第一个维度是相同的还是不应放进去。 而如果反过来,[4, 6] [4,5] 就可以了,出现第二个维度小于自己的时候一定也是第一个维度严格小于的)

显然我们可以 O ( N 2 ) O(N^2) O(N2)的复杂度解决,但是我们观察发现其实也可以用二分压缩。我们维护一个list ,lit.get(i)表示了长度为 i 的情况下,最后一个元素的大小,因此可以二分的找到可以替换的值。思路非常类似LIS。

class Solution {
     
    public int maxEnvelopes(int[][] envelopes) {
     
        // LIS问题
        // 这里后者必须要倒序,保证不会出现第一个维度一样的情况下出现bug
        Arrays.sort(envelopes, (a,b)->(a[0]!=b[0]?a[0]-b[0]:b[1]-a[1]));
        int n = envelopes.length;
        int[] dp = new int[n];
        List<Integer> list = new ArrayList<>();
        list.add(envelopes[0][1]);
        for (int i = 1;i<n;i++){
     
            int[] cur = envelopes[i];
            if (cur[1]>list.get(list.size()-1)){
     
                list.add(cur[1]);
            }else{
     
                int index = bSearch(list, cur[1]);
                list.set(index, cur[1]);
            }
        }
        return list.size();

    }
    
    private int bSearch(List<Integer> nums, int target){
     
        int n = nums.size();
        int l = 0;
        int r = n-1;
        while(l<=r){
     
            int mid = (r-l)/2+l;
            if (nums.get(mid)>target){
     
                r = mid-1;
            }else if(nums.get(mid)<target){
     
                l = mid+1;
            }else {
     
                return mid;
            }
        }
        return l;
    }
}

编辑距离

打家劫舍类

740. 删除并获得点数

【专题讲解】序列DP_第2张图片

class Solution {
     

    // 维护出来了一个sum数组,表示如果要val元素,可以得到多少分。
    public int deleteAndEarn(int[] nums) {
     
        int maxVal = 0;
        for (int val : nums) {
     
            maxVal = Math.max(maxVal, val);
        }
        int[] sum = new int[maxVal + 1];
        for (int val : nums) {
     
            sum[val] += val;
        }
        return rob(sum);
    }
    // 类似打家劫舍 抉择,当前这个要还是不要。
    public int rob(int[] nums) {
     
        int size = nums.length;
        // first 要
        int prev = nums[0], cur = Math.max(nums[0], nums[1]);
        for (int i = 2; i < size; i++) {
     
            int temp = cur;
            // 要前前一个和当前的,还是要前一个不要当前的
            cur = Math.max(prev + nums[i], cur);
            prev = temp;
        }
        return cur;
    }
}

446. 等差数列划分 II - 子序列

【专题讲解】序列DP_第3张图片

题目特点依然是遍历到的当前数字应该加入哪个位置,我们使用一个map保存了每个 d 对应的方案数即可。

class Solution {
     
    // 参考注释,有详细解答
    public int numberOfArithmeticSlices(int[] nums) {
     
        // 每个 f[i] 均为哈希表,哈希表键值对为 {d : cnt}
        // d : 子序列差值
        // cnt : 以 nums[i] 为结尾,且差值为 d 的弱等差子序列数量
        // 若只有两个元素称为弱等差 
        int ans = 0;
        int n = nums.length;
        Map<Long, Integer>[] f = new Map[n];
        for (int i = 0; i < n; ++i) {
     
            f[i] = new HashMap<Long, Integer>();
        }
        for (int i = 0; i < n; ++i) {
     
            for (int j = 0; j < i; ++j) {
     
                long d = 1L * nums[i] - nums[j];
                int cnt = f[j].getOrDefault(d, 0);
                ans += cnt;
                // 弱等差,包括原来的,和num[i]和nums[j],以及全部加上num[j]结尾的
                f[i].put(d, f[i].getOrDefault(d, 0) + cnt + 1);
            }
        }
        return ans;

    }
}

你可能感兴趣的