1. 题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是[2,3,7,101]
,它的长度为4
。
说明:
- 可能会有多种最长上升子序列的组合,只需要输出对应的长度即可。
- 算法时间复杂度应该为O( n 2 n^2 n2)
进阶
将算法的时间复杂度降低到O(nlogn)
2. 解题思路:动态规划
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
nums[i] | 10 | 9 | 2 | 5 | 3 | 7 | 101 | 18 |
dp[i] | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
- 定义dp[i]:以nums[i]为结尾的最长上升子序列的长度
- 从dp[0]开始计算,在计算dp[i]之前,已经算出dp[0]~dp[i-1]的值。
- 考虑比 i 小的每一个 j ,如果有nums[j] < nums[i],那么:dp[i] = max(dp[j]) + 1;如果对所有的j,都有nums[j] >= nums[i],则:dp[i] = 1。
- 状态转移方程为:
dp[i] = max(dp[j]) + 1;( 0 <= j < i 且 nums[j] < nums[i] )
dp[i] = 1; ( 0 <= j < i 且 nums[j] >= nums[i]) - 整个数组的最长上升子序列的长度即为所有dp[i]中的最大值:
LIS = max(dp[i]) (0 <= i < n)
代码
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(n <= 1) return n;
vector<int> dp(n,0);
for(int i=0; i<n; i++) {
dp[i] = 1;
for(int j=0; j<i; j++) {
if(nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j]+1);
}
}
}
sort(dp.begin(), dp.end());
return dp[n-1];
}
复杂度分析
- 时间复杂度:O(n2),其中n为数组nums的长度。动态规划的状态数为n,计算状态dp[i]时,需要O(n)的时间遍历dp[0, … , i-1]的所有状态,所以总时间复杂度为O(n2)。
- 空间复杂度:O(n),需要额外使用长度为n的dp数组。
3. 进阶思路:贪心+二分查找
优化思路:
考虑一个简单的贪心:如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
基于上面的贪心思路,维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值。
我们依次遍历数组 nums 中的每个元素,并更新数组 d。如果nums[i] > d.back() 则将nums[i]添加至数组d的末尾,否则在数组 d 中找满足 d[ans - 1] < nums[i] < d[ans] 的下标 ans,并更新 d[ans] = nums[i]。
根据数组 d 的单调性,我们可以使用二分查找寻找下标 ans,优化时间复杂度。
- 设计思路: 假如我们需要新增一个元素nums[i],希望找出插入nums[i]之后的最长子序列。
- 结论一: 需要在当前允许插入(子序列尾数 < nums[i])的最长子序列后添加元素nums[i]
- 结论二: 只需要维护一个数组d,来保存升序子序列的最小结尾数字,这样通过比较数组d的尾数与nums[i]的大小,就可以知道是能插入数组d还是需要更新数组d的尾数。
- 结论三: 数组d一定是严格递增的
算法步骤:
- 维护一个数组d,d[i]代表长度为 i+1 的上升子序列的最小尾部元素值。
- 对nums[i]:如果d.back() < nums[i],在数组d末尾添加元素nums[i];如果d.back() > nums[i],需要找到数组d中第一个大于nums[i]的元素位置ans,并更新d[ans] = nums[i]。
- 因为数组d是有序的,因此可以通过二分法找出数组中第一个大于nums[i]的数字,及其对应下标ans。
代码
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if(n <= 1) return n;
vector<int> d;
for(int i=0; i<n; i++) {
int k = nums[i];
if(d.empty() || k > d.back()) {
d.push_back(k);
}
else { // k <= d.back() 二分查找
int l = 0;
int r = d.size()-1;
int ans = d.size();
while (l <= r) {
int m = l + (r - l) / 2;
if(d[m] >= k) {
r = m - 1;
ans = m;
}
else {
l = m + 1;
}
}
d[ans] = k;
}
}
return d.size();
}
复杂度分析
- 时间复杂度:O(nlogn)。数组nums的长度为n,我们依次用数组中的元素去更新数组d,更新时使用二分查找进行搜索需要O(logn),所以总时间复杂度为O(nlogn)。
- 空间复杂度:O(n)。需要额外使用长度为n的d数组。