300. 最长递增子序列
难度 中等
给你一个整数数组 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
-104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
题解
这道题以前有了解过,但是忘记的差不多了,只记得是动态规划的题目。然后看了以前收藏的博客,从新学习和实现了一遍。
动态规划实现有两个方法,一种是O(n^2)的实现方法,另外一种是O(nlogn)的实现方法。
动态规划 O(n^2)
那这里我们就需要弄清楚状态转移方程,状态是怎么进行转移的。
如果 i > j,且nums[i] > nums[j],就是在 i 位置的数大于 j 位置的数,那么最长上升子序列长度加一,即dp[i] = dp[j] + 1。
例如10,9,2,5,3,7,101,18,假设变量到101时,此时的最长上升子序列为2,3,7,长度为3。又101大于7,所有此时最长上升子序列为2,3,7,101,长度为4。
所以动态转移方程为if( i > j && nums[i] > nums[j]),dp[i] = dp[j] + 1
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;//数组长度
int[] dp = new int[n];//动态规划数组
int ans = 1;//最短长度为1
for(int i = 0; i < n; i++){
dp[i] = 1;//初始化长度为1,每个元素都是一个最短的最长上升子序列
for(int j = 0; j < i; j++){
if(nums[j] < nums[i]){//如果满足 i > j && nums[i] > nums[j],更新动态规划数组
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
ans = Math.max(ans, dp[i]);//每次都比较最长上升子序列的长度
}
return ans;
}
}
动态规划 + 贪心 O(nlogn)
为了尽可能求的更长的最长上升子序列,我们每次在最长上升子序列尾上升尽可能小的元素。那我们就需要借助一个数组来记录当前的最长上升子序列,然后我们每次遍历到的元素都放进去这个数组,替换最长上升子序列中对应位置的元素,讲的话比较抽象,就举个例子。
10,9,2,5,3,7,101,18,当我们遍历到5的时候,最长上升子序列为2,5,然后又继续遍历。到了3,此时为了让后面能更长,此时选择3做为末尾是最佳的(贪心,选最小的)。
另外,我们替换元素时,需要查找到元素在最长上升序列的哪个位置,因为我们是最长上升子序列,这个序列是有序的,我们可以利用有序数组进行二分查找,提高查找效率,将查找时间从O(n)降为O(logn)
class Solution {
public int lengthOfLIS(int[] nums) {
int[] low = new int[nums.length + 1];//辅助数组记录最长上升子序列
int ans = 1;
low[ans] = nums[0];//初始化
for(int i = 1; i < nums.length; i++){
if(nums[i] > low[ans]){//如果第i个元素大于末尾元素,直接插入末尾,然后指针右移
low[++ans] = nums[i];
}else{//如果小于末尾元素,需要插入有序数组内部,利用有序数组二分查找位置
low[bin_search(low, ans, nums[i])] = nums[i];
}
}
return ans;
}
int bin_search(int[] a, int r, int x){//二分查找
int l = 1;
while(l <= r){
int mid = l + (r - l) / 2;
if(a[mid] < x){
l = mid + 1;
}else{
r = mid - 1;
}
}
return l;
}
}