1. 题目
2. 解法(动态规划)
2.1 解题思路
比暴力法(使用回溯法遍历所有子序列,判定严格上升序列,并取最大值,时间复杂度O(n*2^n))
好,动态规划,分别计算以每个元素结束的上升子序列长度,需要向前遍历寻找最长上升子序列,再加1,时间复杂度O(n^2)
2.2 代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// 解空间
// 保存以nums[i]结尾的最长上升子序列长度
// 初始时单个数字为最长上升子序列 默认值为1
vector<int> dp(nums.size(), 1);
int ans = 0; // 答案
for(int i = 0; i < nums.size(); i++){
int max = 0; // 当 j < max 时,子序列长度已经不可能比max大了
for(int j = i; j >= max; j--){
if(nums[i] > nums[j] && dp[j] > max){
max = dp[j];
}
}
// 状态转移公式
// dp[i] = max{ dp[j] | 0 <= j < i, nums[i] > nums[j] } + 1
dp[i] = max + 1;
if(dp[i] > ans){
ans = dp[i];
} // 记录最长上升子序列长度
}
return ans;
}
};
3. 解法(贪心算法)
3.1 解题思路
贪心思想:当目前保存的上升序列末尾越小,越可能延长
在遍历nums
的过程中,保存长度为i
的上升序列的最小末尾为tail[i]
- 当
nums[i]
大于所有tail
(即 >tail[max_length]
) 时,说明当前维护的最长上升序列末尾应追加元素,使tail
追加元素nums[i]
,max_length
+1 - 当
tail
中存在等于nums[i]
的元素,说明以nums[i]
结尾的上升序列已存在 - 替换第一个比
nums[i]
大的tail
,说明tail
该位置对应长度的上升序列可以有一个更小的末尾【注:此操作不会影响更长的上升序列,而是继承更短的上升序列变化,故贪心有效 (请看 流程解析 )】
tail
在计算过程中始终保持严格单调递增
所以可以使用二分法定位第一个比nums[i]
大的tail
的位置
最终tail
的长度即为最长上升子序列长度
时间复杂度 O(n * logn)
3.2 流程解析
示例:[10, 9, 2, 5, 3, 7, 101, 18, 6, 22, 11, 9]
以下为上升序列搜索和维持示意,
其中各序列末尾为tail
元素,行数自上而下为序列长
最后一行为当前最长上升序列(与tail
不一定一样)
10 :长度为1的上升序列
9 :替换缩小
2 :替换缩小
2
2 5 :延伸长度1的序列为长度2
2
2 3 :替换缩小长度为2的序列末尾元素
2
2 3
2 3 7 :延伸长度2的序列为长度3
2
2 3
2 3 7
2 3 7 101 :延伸长度3的序列为长度4
2
2 3
2 3 7
2 3 7 18 :替换缩小长度为4的序列末尾元素
2
2 3
2 3 6 :替换缩小长度为3的序列末尾元素
2 3 7 18 》 此时6在18右侧,所以7不变
2
2 3
2 3 6
2 3 7 18 》 此时由于6在18右侧,只能延长第4行,不受之前行影响
2 3 7 18 22 :延伸长度4的序列为长度5
2
2 3
2 3 6
2 3 6 11 :替换缩小长度为4的序列末尾元素,可继承更短序列
2 3 7 18 22 》此时由于11在22右侧,不能继续继承
2
2 3
2 3 6
2 3 6 9 :替换缩小长度为4的序列末尾元素
2 3 7 18 22 》此时由于9在18右侧,不能继承
最终最长上升子序列【2 3 7 18 22】
长度为 5
3.2 代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// 辅助空间 在计算过程中保持严格单增
// tail[i] = 长度为i的上升子序列的最小末尾
vector<int> tail(nums.size() + 1, INT_MIN);
int max_length = 0; // 已知最长上升子序列长度
for(int i = 0; i < nums.size(); i++){
if(nums[i] > tail[max_length]){
// 如果num[i]严格大于所有tail,则追加
// 在计算过程中保持严格单增
// 即最长上升子序列+1,并以num[i]结尾
tail[++max_length] = nums[i];
}else{
// 使用二分搜索定位第一个大于num[i]的tail
int l = 0, r = max_length;
while(l <= r){
int mid = (l + r) >> 1; // 位运算 除以2
if(tail[mid] < nums[i]){
l = mid + 1;
}else{
// 当tail中含有相等num[i]的元素时不必进行替换操作,此处为了写法简便,进行了替换
r = mid - 1;
}
}
// 替换(缩小)第一个大于num[i]的上升子序列的最小末尾
tail[l] = nums[i];
}/*
for(int q = 1; q <= max_length; q++){
cout<<tail[q]<<" ";
}
cout<<endl;*/
}
// 最终tail的长度即为最长上升子序列长度
return max_length;
}
};
3.3 疑问
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// 辅助空间 在计算过程中保持严格单增
// tail[i] = 长度为i的上升子序列的最小末尾
vector<int> tail(nums.size() + 1, INT_MIN);
int max_length = 0; // 已知最长上升子序列长度
for(int i = 0; i < nums.size(); i++){
if(nums[i] > tail[max_length]){
// 如果num[i]严格大于所有tail,则追加
// 在计算过程中保持严格单增
// 即最长上升子序列+1,并以num[i]结尾
tail[++max_length] = nums[i];
}else{
// 使用二分搜索定位第一个大于num[i]的tail
int l = 0, r = max_length;
int mid = 0;
while(l < r){
mid = (l + r) / 2;
if(tail[mid] == nums[i]){
// 当tail中含有相等num[i]的元素时不能进行替换操作
mid = -1;
break;
}else if(tail[mid] < nums[i]){
l = mid + 1;
}else{
r = mid - 1;
}
}
if(mid != -1){
// 替换(缩小)第一个大于num[i]的上升子序列的最小末尾
// 此处即使不进行if-else判断也可以通过LeetCode
// 但是这样并没有保证每次换掉的是第一个更大的
// tail序列将会不同,但长度不变
// 可能的猜想:
// 1. 算法可以优化,可能不需要保存所有之前更短的状态
// 2. LeetCode判例不足
//if(tail[l] > nums[i])
tail[l] = nums[i];
//else
// tail[l+1] = nums[i];
}
}
// for(int q = 1; q <= max_length; q++){
// cout<<tail[q]<<" ";
// }
// cout<<endl;
}
// 最终tail的长度即为最长上升子序列长度
return max_length;
}
};
经测试,
[11,12,13,14,15,16,1,2,3,4,1,5,6,7,1,8]
无法通过该判例,但LeetCode官网可以提交通过