最长上升子序列--LeetCode300

最长上升子序列–LeetCode300

题目

给定一个无序的整数数组,找到其中最长上升子序列的长度。示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(nlog n) 吗?

方法一:动态规划

首先定义dp[i] 为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i] 必须被选取 。从左往右计算dp数组中各个位置的值,状态转移方程为:

dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]

含义:

  • dp[0…i−1] 中最长的上升子序列后面再加一个 nums[i]
  • 上面这一项的条件:由于 dp[j] 代表 nums[0…j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j]这个状态转移过来,那么 nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。(例如:2,3,5,3,4,4这个位置上的最长子序列和5就没有关系了,因为4已经不可能接在5的后面构成最长上升子序列了)

故最终的结果就是dp数组中的最大值。
时间复杂度:O(n2),空间复杂度:O(n)

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 边界条件
        if (nums.length == 0) {
            return 0;
        }
        int[] dp = new int[nums.length];//dp数组用于保存每个位置上的最长上升子序列
        dp[0] = 1;//首先第一个位置自身就是一个子序列,长度为1
        // 结果至少是1,所以初始化为1
        // maxans就是dp数组中的最大值
        int maxans = 1;
        // 第一层循环,对dp数组进行遍历,为了确定每个位置上的目标序列长度
        for (int i = 1; i < dp.length; i++) {
            int maxval = 0;
            for (int j = 0; j < i; j++) {
                // 下面的判断条件可以理解为:如果nums[j] >= nums[i]
                // 那么nums[j]位置上的最长上升子序列和num[i]就没有关系了,
                // 就不构成上升序列了
                if (nums[j] < nums[i]) {
                    maxval = Math.max(maxval, dp[j]);
                }
            }
            dp[i] = maxval+1;// 参考状态方程,上面if条件也是
            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }
}

方法二:贪心+二分查找

首先需要明确方法二相对于方法一的动态规划,优化的地方在哪?
动态规划中,遍历计算 dp 数组需 O(N),计算每个 dp[k] 需 O(N)。

  1. 动态规划中,通过线性遍历来计算 dp 的复杂度无法降低;
  2. 每轮计算中,需要通过线性遍历 [0,k)区间元素来得到 dp[k] 。我们考虑:是否可以通过重新设计状态定义,使整个 dp 为一个排序列表;这样在计算每个 dp[k] 时,就可以通过二分法遍历 [0,k) 区间元素,将此部分复杂度由 O(N) 降至 O(logN)。

数组定义:tails[k] 的值代表 长度为 k+1 子序列 的尾部元素值。

转移方程 : 设 res为 tails 当前长度,代表直到当前的最长上升子序列长度。设 j∈[0,res),考虑每轮遍历 nums[k] 时,通过二分法遍历 [0,res) 列表区间,找出 nums[k] 的大小分界点,会出现两种情况:

  • 区间中存在 tails[i]>nums[k] : 将第一个满足 tails[i]>nums[k] 执行 tails[i]=nums[k] ;因为更小的 nums[k] 后更可能接一个比它大的数字。
  • 区间中不存在 tails[i]>nums[k] : 意味着 nums[k] 可以接在前面所有长度的子序列之后,因此肯定是接到最长的后面(长度为 res ),新子序列长度为 res+1。

举例说明:假设此时tail=[2,3,7],num[k]=5,那么[2,3,5]比[2,3,7]更有可能构成最长上升子序列,因为[2,3,5]上升得慢,所以把tail数组更新为[2,3,5];如果num[k]=8,8>7,那么只能把8加入tail数组中。对于查找的过程使用二分查找,例如tail=[2,3,7,10],num[k]=4,那么使用二分查找找到第一个大于4的位置将其更新为4,也就是把7更新为4。

参考资料:力扣(LeetCode)-Krahets的解题

class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int[] tail = new int[nums.length];
        int res = 0;//tail数组的当前长度
        for (int num : nums) {
            // 二分查找,寻找tail数组中元素第一个大于num的位置
            int i = 0, j = res;
            while (i < j) {
                int mid = (i+j)>>1;
                if (tail[mid] < num) {
                    i = mid+1;
                }else {
                    j = mid;
                }
            }
            tail[i] = num;// 注意这里为什么是i而不是j!从含义上去理解,二分查找找的是第一个大于num的位置
            if (j == res) {
                // 说明num比tail数组中所有元素都大,那么在tail数组长度加一
                res++;
            }
        }
        return res;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值