300. Longest Increasing Subsequence
Given an unsorted array of integers, find the length of longest increasing subsequence.
Example
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Note
Note:
- There may be more than one LIS combination, it is only necessary for you to return the length.
- Your algorithm should run in
O(n^2)
complexity.
Follow up:
Could you improve it to
O(n log n)
time complexity?
思路
最近几周算法课都没有讲新的算法内容,一直在讲动态规划的习题,大概了也说明DP有多么的重要了。DP这个算法真的让人又爱又恨,它的代码量并不大,关键代码其实就状态转移方程那几行,但要找出正确的状态转移方程是一个十分伤脑筋的过程,有时候难起来真的要人命。经过这几周的练习,深深感觉到我与大佬之间的差距,知道自己有多菜,但前进的脚步不能停下来,还是要尽自己所能去挑战一些有难度的动态规划题目的,所以这周选了这一道题。
题目相当简洁明了,就是在一串乱序数组里找最长的上升序列,数字之间不需要连续,其实这也是废话,如果数字要求连续的话也就不关动态规划什么事了,简简单单把数组遍历一遍就能找到最长上升子串了。Note里说让我们先用时间复杂度为O(n^2)
的方法做,这个算挺简单的,不一会就做出来。接着让我们尝试用时间复杂度O(n log n)
的方法,这还是有一点难度的,不过最后还是花费了不少时间做出来了。
先来说下O(n^2)
的方法吧,当前状态的最大上升子串肯定是由前面状态的最大上升子串加1得到的。因为不是重点就不详细讲了,状态转移方程和具体的代码解答如下:
j∈[0…i-1]
dp[i] = max(dp[i], dp[j] + 1) // if nums[i] > nums[j]
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int ans = 0;
int index = 0;
int *dp = new int[nums.size()];
for (int i = 0; i < nums.size(); i++)
{
dp[i] = 1;
for (int j = 0; j < i; j++)
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
}
delete []dp;
return ans;
}
int max(int num1, int num2)
{
if (num1 > num2) return num1;
else return num2;
}
};
接下来我们来讲一下O(n log n)
的解法。提到动态规划里的log n
的话,首先想到的肯定是二分了。问题是怎么利用二分,排序?肯定不对,仅仅是二分排序的时间复杂度就有O(n log n)
了,算上DP的时间肯定超了。那么是查找?因为题目是找上升子串,在有序数列里用二分查找,好像可行?所以我就按着这个方向去考虑了。
我们可以用一个数组把目前找到的最长上升子串存起来,当访问到一个新节点时,看情况选择把它放进数组或者是不放,判断的标准就用二分查找的结果,最后这个数组的长度就是答案。因为对每个节点我们用二分查找的结果来判断情况,不用把前面的所有状态节点遍历一遍,所以时间复杂度是O(n log n)
而不是O(n^2)
。判断的标准如下:
- 如果能在数组里查找到数字num,那么保持数组状态不变
- 如果不能找到:
- 如果num < LIS[0],LIS[0] = num
- 如果num < LIS[len - 1],LIS[len - 1] = num,len为数组长度
- 其余情况,找到有序数组里第一个比num大的数字,num替代它在数组里的位置
显而易见的,按照上面的做法得到的LIS数组,里面数字的相对顺序跟原数列的相对顺序是不同的,不符合题意。然而这并不碍事,即使数字的顺序不对,它所处的位置却是对的,假如num在LIS数组的第3位,证明前面有2个数字比它小,也许经过LIS数组的更新把num后面的数字放在了它前面,但事实上确实有一个符合题意的长度为3的子串曾经在LIS数组里出现过。
O(n log n)
方法的思路大概就这样了。在写代码时还有一点要注意的,因为我们二分查找的目的是找到LIS数组里第一个比num大的数字,并不是平时那样在数组里知道num的位置,因此要改变一下二分查找的函数,注意不要陷进死循环。
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int ans = 0;
int index = 0;
int *LIS = new int[nums.size()];
for (int i = 0; i < nums.size(); i++)
{
index = binarySearch(nums[i], LIS, ans);
LIS[index] = nums[i];
if (index + 1 > ans)
ans = index + 1;
}
delete []LIS;
return ans;
}
int binarySearch(const int num, int* LIS, const int& len)
{
if (len == 0 || num < LIS[0]) return 0;
if (num > LIS[len - 1]) return len;
int head = 0;
int tail = len - 1;
int mid;
while (true)
{
mid = (head + tail) / 2;
if (LIS[mid] == num || (LIS[mid] > num && LIS[mid - 1] < num)) break;
else if (LIS[mid] > num) tail = mid -1;
else head = mid + 1;
}
return mid;
}
};