打赏

leetcode 300. 最长上升子序列

题目描述

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

示例:

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

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

代码

代码1. 动态规划

解题思路:

状态定义:

  dp[i] 代表 nums 前 i 个数字的最长子序列长度。

转移方程:

  设 j∈[0,i),考虑每轮计算新 dp[i] 时,遍历 i 之前的 dp 列表区间(即 [0,i) 区间);
  当 nums[i] > nums[j] 时: nums[i]可以接在 nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为 dp[j] + 1 ;
  当 nums[i] <= nums[j]时: nums[i]无法接在 nums[j]之后,此情况上升子序列不成立,跳过。
  上述所有情况下计算出的 dp[j] + 1 的最大值,为直到 i 的最长上升子序列长度,实现方式为遍历 j 时每轮执行 dp[i] = max(dp[i], dp[j] + 1)

 初始状态:令 dp 列表所有值 =1,含义为每个数字单独组成序列时,长度都为 1 。

 返回值:返回 dpdp 列表的最大值,即最终最长上升子序列。



复杂度分析:
  时间复杂度 O(N^2): 遍历计算 dp 列表需 O(N) ,计算每个 dp[i] 需 O(N) 。
  空间复杂度 O(N)O(N) : dpdp 列表占用线性大小额外空间。

粗暴总结:
  遍历dp, 找 i 前面的所有小于 nums[i] 的数字的 max(dp)+1

代码

// Dynamic programming.
public class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return 0;
        }
        int[] dp = new int[len]; //dp[i] 代表 nums 前 i个数字的最长子序列长度。
        dp[0] = 1;
        int max = 1; //保存最长上升序列长度
        for (int i = 1; i < len; i++) {
            int maxj = 0; //保存 i 前面的所有小于 nums[i] 的数字中的最大dp[j]值;
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    maxj = Math.max(maxj, dp[j]);
                }
            }
            dp[i] = maxj + 1;
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

精简版本

//Dynamic programming.
class Solution {
 public int lengthOfLIS(int[] nums) {
     int len = nums.length;
     if (len == 0) {
         return 0;
     }
     int[] dp = new int[len];
     int max = 0;
     Arrays.fill(dp, 1);//令 dp列表所有值 =1, 含义为每个数字单独组成序列时,长度都为 1 。
     for(int i = 0; i < len; i++) {
         for(int j = 0; j < i; j++) {
             if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
         }
         max = Math.max(max, dp[i]);
     }
     return max;
 }
}

 

代码2. 动态规划+二分查找

解题思路:

降低复杂度切入点: 
    遍历计算 dp 列表需 O(N) ,计算每个 dp[i] 需 O(N) 。
    动态规划中,通过线性遍历来计算 dp 的复杂度无法降低;
    每轮计算中,需要通过线性遍历 [0,i) 区间元素来得到 dp[i]。

改进思路:
    设常量数字 N ,和随机数字 x ,我们可以容易推出:当 N 越小时,N<x 的几率越大。
    重新设计状态定义,使整个 dp 为一个排序列表;
    这样在计算每个 dp[i] 时,就可以通过二分法遍历 [0,i) 区间元素,将此部分复杂度由 O(N)降至 O(logN) 。


状态定义:

    dp[i] 的值代表 子序列的长度 为 i 时,此序列尾部元素的值。

转移方程:
    
  遍历计算每个 dp[i],不断更新长度为 [
1,i] 的子序列尾部元素值,始终保持每个尾部元素值最小 初始状态:
  令 dp 列表所有值
=1 ,含义为每个数字单独组成序列时,长度都为 1 。 返回值:
  返回 dp 列表的最大值,即最终最长上升子序列。

代码

//Dynamic programming + Dichotomy.
class Solution {
 public int lengthOfLIS(int[] nums) {
     int[] dp = new int[nums.length];//dp[i] 的值代表 子序列的长度 为 i 时,此序列尾部元素的值。
     int max = 0;// 最长子序列的长度
     for(int num : nums) {
         int i = 0, j = max;
         while(i < j) {
             int m = (i + j) / 2;
             if(dp[m] < num) i = m + 1;
             else j = m;
         }
         dp[i] = num; //二分法找到大于num的第一个值覆盖
         max = Math.max(max, i + 1);//更新最大长度
     }
     return max;
 }
}

 

posted @ 2019-09-04 18:35  海米傻傻  阅读(273)  评论(0编辑  收藏  举报