动态规划 —— 最长上升子序列全解

题目链接:300. 最长递增子序列 - 力扣(LeetCode)

朴素做法

设元素数组为arr,定义一维数组dp,dp[i]表示以i位置结尾的子序列中,最长的上升子序列的长度

这些子序列可以被划分成哪些子集合呢?

一般集合划分用最后一个位置分类,但这里最后一个位置限定了是i,则可以按照倒数第二个位置进行分类:

  • 没有倒数第二个数
  • 倒数第二个数为arr[0]
  • 倒数第二个数为arr[1]
  • 倒数第二个数为arr[i-1]

在这里插入图片描述

当然不是每一个分类都存在,因为有一个前提倒数第二个数比arr[i]小,才能构成上升子序列

假设倒数第二个数为arr[j],最后一个数为arr[i],则以i结尾的子序列中最长上升子序列的长度,就是

以j结尾的子序列中最长上升子序列长度+1

由于是从左往右推导dp数组,因此在计算dp[i]时,dp[j]的值一定已经计算好

我们尝试所有倒数第二个位置的分类,这些分类的最大值,就是以i为结尾的最长上升子序列长度

dp[i] = max(dp[j]) + 1,arr[j] < arr[i],j从0到i-1

public int lengthOfLIS(int[] arr) {
    int[] dp = new int[arr.length];
    dp[0] = 1;
    int max = 1;
    for (int i = 1;i<arr.length;i++) {
        dp[i] = 1;
        for (int j = 0;j < i;j++) {
            if (arr[i] > arr[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
                max = Math.max(max, dp[i]);
            }
        }
    }
    
    return max;
}

终极做法

朴素做法的时间复杂度为O(N^2),还有更好的做法,时间复杂度能降到O(NlogN)

我们维护一个end数组,end[i]表示:

长度为i的最长上升子序列中,结尾最小是多少

在这里插入图片描述

end数组是单调递增的

假设不是单调底层的,假设i = j + 1,end[i] <= end[j],

长度为i的上升子序列的最小值为end[i],那么这个子序列中第j个数一定小于end[i],而假设中end[j]大于等于end[i],和假设矛盾,因此end数组一定是单调递增的

当我们遍历到第k个数时,之前的数构成的上升子序列信息已经存到end数组中了

假设end中最大的长度为right

如果arr[i]比end[right]大,说明可以将arr[i]压到end[right]后面,构造出一个更长的上升子序列right++

否则在end数组中,找第一个大于等于 arr [i]的数end[l]

因为end数组单调递增,可以用二分法查找

arr[i]替换掉原来的end[l],使其变得更小:

  • 因为arr[i]大于end[l-1],将arr[i]放在end[l]位置不破坏end数组的定义
  • 原来的end[l]大于等于arr[l],替换后只可能使得end[l]更小,维持了长度为l的上升子序列中结尾最小值的定义

代码实现如下:

public int lengthOfLIS(int[] arr) {
    int[] end = new int[arr.length+1];
    int l = 0;
    int r = 0;
    int right = 1;
    end[1] = arr[0];

    for (int i = 1;i<arr.length;i++) {
        l = 1;
        r = right;

        if (arr[i] > end[r]) {
            end[++right] = arr[i];
            continue;
        }

        // 找到大于等于arr[i]的第一个数
        while (l <= r) {
            if (l == r) {
                break;
            }
            int mid = l + r >> 1;
            if (arr[i] <= end[mid]) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }

        // 将arr[i]替换掉原来的end[l],使其更小
        end[l] = arr[i];
    }

    return right;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值