leetcode-master/problems/0300.最长上升子序列.md

9.9 KiB
Raw Blame History

欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

300.最长递增子序列

题目链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

  示例 1 输入nums = [10,9,2,5,3,7,101,18] 输出4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2 输入nums = [0,1,0,3,2,3] 输出4

示例 3 输入nums = [7,7,7,7,7,7,7] 输出1   提示:

  • 1 <= nums.length <= 2500
  • -10^4 <= nums[i] <= 104

方法一 动态规划

思路

最长上升子序列是动规的经典题目这里dp[i]是可以根据dp[j] j < i推导出来的那么依然用动规五部曲来分析详细一波

  1. dp[i]的定义

dp[i]表示i之前包括i的最长上升子序列

  1. 状态转移方程

位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

所以if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

注意这里不是要dp[i] 与 dp[j] + 1进行比较而是我们要取dp[j] + 1的最大值

  1. dp[i]的初始化

每一个i对应的dp[i]即最长上升子序列起始大小至少都是是1.

  1. 确定遍历顺序

dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来那么遍历i一定是从前向后遍历。

j其实就是0到i-1遍历i的循环里外层遍历j则在内层代码如下

for (int i = 1; i < nums.size(); i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
    }
    if (dp[i] > result) result = dp[i]; // 取长的子序列
}
  1. 举例推导dp数组

输入:[0,1,0,3,2]dp数组的变化如下

300.最长上升子序列

如果代码写出来但一直AC不了那么就把dp数组打印出来看看对不对

以上五部分析完毕C++代码如下:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        int result = 0;
        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
            }
            if (dp[i] > result) result = dp[i]; // 取长的子序列
        }
        return result;
    }
};

总结

本题最关键的是要想到dp[i]由哪些状态可以推出来并取最大值那么很自然就能想到递推公式dp[i] = max(dp[i], dp[j] + 1);

子序列问题是动态规划的一个重要系列,本题算是入门题目,好戏刚刚开始!

其他语言版本

Java

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

Python

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if len(nums) <= 1:
            return len(nums)
        dp = [1] * len(nums)
        result = 0
        for i in range(1, len(nums)):
            for j in range(0, i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
            result = max(result, dp[i]) #取长的子序列
        return result

Go

func lengthOfLIS(nums []int ) int {
  dp := []int{}
  for _, num := range nums {
      if len(dp) ==0 || dp[len(dp) - 1] < num {
      dp = append(dp, num)
    } else {
          l, r := 0, len(dp) - 1
          pos := r
          for l <= r {
              mid := (l + r) >> 1
              if dp[mid] >= num {
                  pos = mid;
                  r = mid - 1
              } else {
                  l = mid + 1
              }
          }
          dp[pos] = num
      }//二分查找
  }
    return len(dp)
}

Javascript

const lengthOfLIS = (nums) => {
    let dp = Array(nums.length).fill(1);
    let result = 1;

    for(let i = 1; i < nums.length; i++) {
        for(let j = 0; j < i; j++) {
            if(nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j]+1);
            }
        }
        result = Math.max(result, dp[i]);
    }

    return result;
};

复杂度分析

  • 时间复杂度O(n^2)。数组 nums 的长度为 n我们依次用数组中的元素去遍历 dp 数组,而遍历 dp 数组时需要进行 O(n) 次搜索,所以总时间复杂度为 O(n^2)。
  • 空间复杂度O(n),需要额外使用长度为 n 的 dp 数组。

方法二 贪心策略+二分搜索

使用贪心策略和二分搜索可以进一步将算法时间复杂度将为O(nlogn)。

思路

为了使得到的子序列尽可能长,我们需要使序列上升得尽可能慢。

对于长度为n的数组 nums我们从0到n-1依次遍历数组中的每个元素nums[i],更新在0到i范围内最长上升子序列的长度len以及 在0到i范围内上升子序列的长度为1到len时对应长度子序列最右端的最小值将结果保存在list中。实际编码过程中list长度即为len。

可行性

当我们遍历完数组nums中第n-1个元素时list中保存的是0到n-1范围内最长上升子序列的长度即为所求。

算法复杂度分析

  1. list中的元素是单调递增的。可以用反证法来证明假设对于0<=i<j<len有list[i]>=list[j]那么我们可以在list[j]对应的子序列中删除最后j-i个元素得到长度与list[i]相同的子序列其最右端的值max<list[j]<=list[i],与list的定义矛盾。

  2. 假设我们已经得到0到i-1范围内对应的list,我们可以在O(logn)的时间复杂度内更新list,得到0到i范围内的list。

  3. if(nums[i]>list[len-1],此时list中子序列长度为1到len的对应的最右端最小值不变并新增长度为len+1的子序列最右端的最小值为nums[i],时间复杂度O(1);

  4. if(nums[i]<=list[len-1])此时我们可以在0到len-1范围内找到k,list[k]为>=nums[i]的最小值,由于list单调递增所以我们可以使用二分搜索在O(logn)的时间复杂度内找到k。 1. 对于0<=j<k,list[j]<nums[i]恒成立对应list[j]的值不需要更新。 2. 对于list[k]其值更新为nums[i],因为原本list[k]对应的子序列的倒数第二项的值可以=list[k-1]<nums[i]。 3. 对于k<j<=len-1,对应的list[j]不需要更新。因为这些list[j]对应的子序列的倒数第二项的值>nums[i];

  5. 综上算法时间复杂度为O(nlogn),空间复杂度为O(n),需要O(n)的空间保存list。

代码如下

Java

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n==0){return 0;}

        List<Integer> list=new ArrayList<>();
        list.add(nums[0]);
        for (int i = 1; i < n; ++i) {
            if (nums[i] > list.get(list.size()-1)) {
                list.add(nums[i]);
            } else {
                int k=binarySearch(list,nums[i]);
                list.set(k,nums[i]);
            }
        }
        return list.size();
    }

    int binarySearch(List<Integer>list, int num){
      int len=list.size();
      int l=0,r=len-1,ans=len-1;
      while(l<=r){
        int mid=l+(r-l)/2;
        if(list.get(mid)<num){
          l=mid+1;
        }else{
          r=mid-1;
          ans=mid;          
        }
      }
      return ans;
    }
}

实际运行过程中list的长度不会超过n所以我们可以用数组来模拟list代码如下。

Java

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        if(n==0){return 0;}

        //初始化listlen记录list长度
        int[] list=new int[n];
        int len=0;

        //添加元素到list并更新len的值
        list[len++]=nums[0];

        for (int i = 1; i < n; ++i) {
            if (nums[i] > list[len-1]) {
                list[len++]=nums[i];
            } else {
                int k=binarySearch(list,len,nums[i]);
                list[k]=nums[i];
            }
        }
        return len;
    }

    int binarySearch(int[] list,int len, int num){

      int l=0,r=len-1,ans=len-1;
      while(l<=r){
        int mid=l+(r-l)/2;
        if(list[mid]<num){
          l=mid+1;
        }else{
          r=mid-1;
          ans=mid;          
        }
      }
      return ans;
    }
}