题目链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/
题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
思路
蛮力法找出所有子序列,共有 2 n 2^n 2n 个子序列;如果每个子序列都进行检查,则总的时间复杂度为 O ( n ∗ 2 n ) O(n*2^n) O(n∗2n)。复杂度太高,下面考虑动态规划的解法。
1 动态规划O(n^2)
【1】定义状态
令dp[i]
表示以i
元素结尾的最长上升子序列的长度。即在 [0, ..., i]
的范围内,选择以数字 nums[i]
结尾可以获得的最长上升子序列的长度。注意:以第 i 个数字为结尾,即要求 nums[i]
必须被选取。反正一个子序列一定会以一个数字结尾,那我就将状态这么定义,这一点是常见的。
【2】状态转移方程
遍历索引是i
的元素时,需要检查[0,i-1]的所有dp,如果nums[i]
严格大于之前的某个nums[j],则将nums[i]接到这个数后必然能够形成一个更长的上升子序列;此时dp[i]为他们的最大值+1。
状态转移方程:dp(i) = max( 1 + dp(j) if j < i and dp[i] > dp[j])
。
复杂度分析
时间复杂度:O(n^2)
空间复杂度:O(n)
/*
* 最长上升子序列:动态规划
* 时间复杂度O(N^2) 空间复杂度O(N)
*/
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(),1); // 表示以当前元素结尾的最长上升子序列长度;当前元素必须使用
int maxLen = 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); // 如果当前元素i大于j,则序列长度加一
}
maxLen = max(dp[i],maxLen);
}
return maxLen;
}
};
2 动态规划+贪心+二分查找
维护一个数组tail
,当出现的数大于这个数组直接append,否则替换掉数组中大于等于这个数的最小值。
最后tail
的长度就是最长上升子序列的长度
而tail
数组是一个有序数组,使用二分查找复杂度为O(log n)
每一次来一个新的数 num
,就找 tail
数组中第一个大于等于 num
的那个数,试图让它变小,以致于新来的数有更多的可能性接在它后面,成为一个更长的“上升子序列”,这是“贪心算法”的思想。
复杂度分析
时间复杂度:O(nlogn)
空间复杂度:O(n)
/*
* 最长上升子序列:动态规划+贪心+二分查找
* 时间复杂度O(NlogN) 空间复杂度O(N)
*/
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
vector<int> dp = {nums[0]}; // 第i个位置上的元素是长度为i+1的最长上升子序列的末尾元素最小值
for (int i = 1; i < nums.size(); ++i) {
int left = 0, right = dp.size();
// 二分查找第一个大于等于num[i]的数的索引
while (left<right){
int mid = (left+right) /2;
if(dp[mid] < nums[i])
left = mid +1;
else
right = mid;
}
if (left == dp.size())
dp.push_back(nums[i]); // 比最末尾元素大,加入数组
else
dp[left] = nums[i]; // 替换
}
return dp.size();
}
};