给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
输入:nums = [0,1,0,3,2,3]
输出:4
输入:nums = [7,7,7,7,7,7,7]
输出:1
本题动态规划的关键就是 dp[i] ,表示 最后一位是 nums[i] 的最长上升子序列的长度。(注意: nums[i]必须被选取!)
动规五部曲:
dp[i] 表示 i 之前包括 i 的最长上升子序列的长度。
位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[j] < nums[i]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要 dp[i] 与 dp[j] + 1进行比较,而是我们要取 dp[j] + 1的最大值。
每一个 i,对应的 dp[i](即最长上升子序列)起始大小至少都是是1.
dp[i] 是有 0 到 i-1各个位置的最长升序子序列 推导而来,那么遍历 i 一定是从前向后遍历。j 其实就是0到 i-1,遍历i的循环里外层,遍历 j 则在内层。
考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
新建数组 tails,用于保存最长上升子序列。
对原序列进行遍历,将每位元素二分插入 tails 中。
总之,思想就是让 tails 中存储比较小的元素。这样,tails 未必是真实的最长上升子序列,但长度是对的。
tails 列表一定是严格递增的: 即当尽可能使每个子序列尾部元素值最小的前提下,子序列越长,其序列尾部元素值一定更大。
反证法证明: 当 k= tails[i] ,代表较短子序列的尾部元素的值 > 较长子序列的尾部元素的值。这是不可能的,因为从长度为 i 的子序列尾部倒序删除 i−1 个元素,剩下的为长度为 k 的子序列,设此序列尾部元素值为 v,则一定有 v < tails[i](即长度为 k 的子序列尾部元素值一定更小), 这和 tails[k] >= tails[i] 矛盾。
既然严格递增,每轮计算 tails[k]时就可以使用二分法查找需要更新的尾部元素值的对应索引 i。
举个栗子:
例如:[1 2 5 2 3 5 3 4 5]
所以最长上升子序列长度为5,而且我们知道是第一个1,第二个2,第二个3,第一个4,第三个5,所以也可以找到序列位置
public class LengthOfLIS {
public static void main(String[] args) {
// TODO Auto-generated method stub
int [] nums = {10,9,2,5,3,7,101,18};
System.out.println(lengthOfLIS(nums));
}
public static int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
int len = 1;
for(int i = 0; i < n; i++) {
dp[i] = 1;
for(int j = 0; j < i; j++) {
if(nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
len = Math.max(len, dp[i]);
}
return len;
}
}
public class LengthOfLIS {
public static void main(String[] args) {
// TODO Auto-generated method stub
int [] nums = {10,9,2,5,3,7,101,18};
System.out.println(lengthOfLIS(nums));
}
public static int lengthOfLIS(int[] nums) {
int n = nums.length;
int len = 0;
for(int i = 0; i < n; i++) {
int index = binarySearch(nums, len, nums[i]);
nums[index] = nums[i];
if(index == len) {
len++;
}
}
return len;
}
public static int binarySearch(int[] nums, int len, int num) {
// TODO Auto-generated method stub
int l = 0, h = len;
while(l < h) {
int mid = l + (h - l) / 2;
if(nums[mid] == num) {
return mid;
}else if(nums[mid] > num) {
h = mid;
}else {
l = mid + 1;
}
}
return l;
}
}
法一:
法二:
时间复杂度:O(nlogn)。数组 nums 的长度为 n,我们依次用数组中的元素去更新 当前最长子序列数组,而更新数组时需要进行 O(logn) 的二分搜索,所以总时间复杂度为 O(nlogn)。
空间复杂度:O(1),在原数组上记录,不需要额外空间。
题目来源:力扣。