题目描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n²) 。
- 进阶:你能将算法的时间复杂度降低到 O(nlogn) 吗?
一、DP 时间复杂度为O(n²)
动态规划的两种实现
1.自顶向下(带有备忘录的递归)
2.自底向上(记录过程中求得的结果)
最优子结构 + 重叠子问题 -> 动态规划
核心:将问题分解成若干个小问题,并记住求过的解来节省时间。
Those who cannot remember the past are condemned to repeat it.
算法步骤:
1.描述最优解的结构特征(列出状态转移方程)
2.自底向上计算最优解的值(利用递归或循环)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = (int)nums.size();
if (n < 2)
return n;
int* dp = new int[n]; //创建dp数组
for (int i = 0; i < n; i++)
{
dp[i] = 1; //初始最短长度序列中只包含自身
//对于每新加入的nums[i],需要遍历其前面的所有元素
for (int j = 0; j < i; j++) //遍历找到最长上升子序列的长度
if (nums[i] > nums[j]) //如果满足单调增则带入如下方程
{ //状态转移方程
dp[i] = max(dp[i], dp[j]+1);
}
}
int result = dp[0];
for (int i = 0; i < n; i++)
if (dp[i] > result)
result = dp[i];
return result;
}
};
二、贪心算法 + 二分查找 时间复杂度为O(nlogn)
核心思想: 为了上升序列尽可能的长,应该使每次加的值尽可能小。
故引入两个参数:
1.数组d[i]:记录长度为i的上升序列末尾的元素。
2.长度len:记录序列的长度。
步骤:
1.遍历nums[i],若nums[i] > d[len],则长度len++,d[len] = nums[i]。
2.否则,用二分查找找到d[pos] < nums[i] < d[pos+1] 的这样一个位置pos,更新d[pos+1] = nums[i]。(d数组里的元素是递增的)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = (int)nums.size();
if (n < 2)
return n;
int* d = new int[n+1](); //d数组各个元素初始化为0
int len = 1; //长度初始化为1元素为nums[0]
d[1] = nums[0];
for (int i = 1; i < n; i++) //O(n)遍历nums各个元素
{
if (nums[i] > d[len]) //步骤1的代码实现
{
d[++len] = nums[i];
}
else //O(logn)二分查找位置
{
int left = 1, right = len, pos = 0;
while (left <= right)
{
int mid = (left + right) / 2;
if (d[mid] < nums[i])
{
pos = mid;
left = mid + 1;
}
else
{
right = mid - 1;
}
}
d[pos+1] = nums[i];
}
}
return len;
}
};
总结:需要注意的是,d[len]代表的是长度为len的上升子序列末尾值的最小值,加入一个元素时考虑的只有d[len],不需要考虑d的其他元素。步骤二的更新d[len]是贪心算法的核心体现。