LeetCode题解 300 最长上升子序列
1. 题目描述
给定一个数字序列,求其最长上升子序列
1.1. 测试用例
测试用例
int[] nums = {4,2,4,5,3,7};
预期结果
4, 序列是{2,4,5,7}
1.2. 函数签名
public int lengthOfLIS(int[] nums){
}
2. 题解
2.1. 动态规划解法
时间复杂度为
O(N^2)
2.1.1. 分析
- 确定状态:dp[i]以nums[i]结尾的最长子序列的长度
- 转移方程: $dp[i] = max { 1, dp[j] + 1 } \quad 0 \le j < i 且 nums[i] \ge nums[j]$
- 以nums[i]结尾的LIS,序列中nums[i]的上一个数字可能是nums[i]之前的任何一个比它小的数, 假设是上一个数字nums[j], 则有j < i 且 nums[i] $\le$ ge nums[j], 此时的序列的长度为dp[j] + 1
- 也有可能nums[i]之前的数字没有比它小的,那么以它结尾的LIS长度就是1
- 返回值。注意要返回dp数组中的最大值,而不是dp[n]
2.1.2. Java实现
public int longestIncreasingSubsequence(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
//dp[i] : 以nums[i]结尾的LIS的长度
int n = nums.length;
int[] dp = new int[n] ;
//初始化
Arrays.fill(dp, 1);
for(int i = 0; i < n; i++){
//nums[i]之前的每一个数字
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
//dp中的最大值
int res = dp[0];
for (int i = 1; i < n; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
2.2. 二分解法
时间复杂度为
O(N*logN)
,单独去理解比较难,在动态规划的解法上进一步思考会更好理解
2.2.1. 分析
数组{4, 2, 4, 5, 2,7}, 用DP解法可以得到,dp = {1, 1, 2, 3, 2,7}。假设计算dp[3], DP解法在得到dp[3]时,需要顺序查找3之前的dp值中可用的(序列尾元素小于nums[3]的)最大值,其实可以使用二分查找。但想使用二分查找前,先要证明一些结论。
通过dp数组我们知道dp[3]前面的dp有:
- dp[0],dp[1]对应长度为1的序列
{4}、{2}
,它们中最小的尾元素是2 - dp[2]对应长度为2的序列
{*, 4}
, 它们中最小的尾元素是4
结论1 : 数值相等的那些dp只需要查找最小的尾元素是否可用
- 分析:如果尾元素大的可用,尾元素小的一定可用,但是反过来不一定,所以最后的采用的一定会是dp相同的尾元素最小的其中一个
结论2 : dp值递增时,它们中最小的尾元素肯定也是递增的
- 反证法,dp = len1的最小尾元素是t1,dp = len2的最小尾元素是t2, len1 < len2。如果t1 > t2 , 因为长为len2的序列为...t2, 其中肯定有长为len1的序列 ...t3...t2, t3<=t2, 和长为len1的序列的最小尾元素t1 > t2矛盾
有了这两个结论,可以用tail[j]记录dp值为j的序列们的最小尾元素,dp[i]的值可以通过对tail进行二分查找可用的尾元素值来得到
2.2.2. Java实现
public int longestIncreasingSubsequence(int[] nums) {
int n = nums.length;
//minTail[i] 所有长度为i的子序列中最小的尾元素的值
int[] minTail = new int[n + 1];
minTail[0] = Integer.MIN_VALUE;
//最后一个记录的位置
int maxLen = 0;
for (int i = 0; i < n; i++) {
int prePlace = 0;
int start = 0, end = maxLen, mid;
while (start <= end) {
mid = start + (end - start) / 2;
//nums[i]会放到最后一个比它小的堆的右边
if (nums[i] > minTail[mid]) {
prePlace = mid;
start = mid + 1;
} else {
end = mid - 1;
}
}
//更新nums[i]放的堆的最小值
minTail[prePlace + 1] = nums[i];
//所有的堆的最小值都比它小,另起一堆
if (prePlace + 1 > maxLen) {
maxLen = prePlace + 1;
}
}
return maxLen;
}