76 最长上升子序列
原题网址:https://www.lintcode.com/problem/longest-increasing-subsequence/description
描述
给定一个整数序列,找到最长上升子序列(LIS),返回LIS的长度。
您在真实的面试中是否遇到过这个题?
说明
最长上升子序列的定义:
最长上升子序列问题是在一个无序的给定序列中找到一个尽可能长的由低到高排列的子序列,这种子序列不一定是连续的或者唯一的。
https://en.wikipedia.org/wiki/Longest_increasing_subsequence
样例
给出 [5,4,1,2,3]
,LIS 是 [1,2,3]
,返回 3
给出 [4,2,4,5,3,7]
,LIS 是 [2,4,5,7]
,返回 4
挑战
要求时间复杂度为O(n^2) 或者 O(nlogn)
标签
二分法
LintCode 版权所有
动态规划(DP)
O(n^2)思路:
动态规划,dp【i】为以 i 为终点的最长上升子序列的长度。
状态转移方程:对每个i,遍历其之前的动态规划数组,即dp【j】(0<=j<i),如果nums【i】>nums【j】(确定终点),dp【j】加1。然后,在这些更新长度后的dp【j】里找到最大值赋给dp【i】。
最后返回dp数组最大值。
AC代码:
class Solution {
public:
/**
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
int longestIncreasingSubsequence(vector<int> &nums) {
// write your code here
int size=nums.size();
if (size<=0)
{
return 0;
}
vector<int> dp(size,1);
int maxl=0;
for (int i=1;i<size;i++)
{
for (int j=0;j<i;j++)
{
if (nums[i]>nums[j])//不可缺少,因为nums【i】为终点;
{
dp[i]=max(dp[i],dp[j]+1);//更新dp[j],将选出的最大dp[j]赋给dp[i];
}
}
maxl=max(maxl,dp[i]);//更新dp数组最大元素;
}
return maxl;
}
};
最开始以为dp【i】是以 i 为终点的子数组的最长上升子序列长度,汗……如果动态数组定义是这样的,参照上面的思路写出如下代码,运行结果会出错。
如【10,11,1,12,2,10,3,11,4】,相应dp为【1,2,2,3,3,4……】,计算到第二个10便会出错。因为 nums[i]>nums[j] tmp便加1是有问题的,你没法确定 j 处最长上升子序列的末位元素是否为nums【j】,这种方法无法保证子序列一定是递增的。
int longestIncreasingSubsequence(vector<int> &nums)
{
// write your code here
int size=nums.size();
if (size<=0)
{
return 0;
}
vector<int> dp(size,1);
for (int i=1;i<size;i++)
{
for (int j=0;j<i;j++)
{
int tmp=dp[j];
if (nums[i]>nums[j])
{
tmp=tmp+1;
}
dp[i]=max(dp[i],tmp);//更新dp[j],将选出的最大dp[j]赋给dp[i];
}
}
return dp[size-1];
}
参考:
lintcode-最长上升子序列-76 O(n ^ 2)代码更简洁
lintcode:最长上升子序列 讲解详细
O(nlogn)思路:动态规划+二分法
假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到B[5], 9更新到B[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到B[5], 9更新到B[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
大致思路:
dp中存储的是相应长度的LIS的最小末尾。即数组下标代表LIS长度,对应值代表LIS最小末尾。每次取出nums数组中的元素在dp中对比查找时,nums【i】对比的也都是相应LIS的最小末尾。
在dp中找到第一个大于nums【i】的最小末尾,可以用nums【i】替换这个长度下的最小末尾而不必担心LIS递增问题,因为是顺序遍历nums且dp中之前长度的最小末尾均小于nums【i】。
如果dp中所有末尾均小于nums【i】,说明LIS的长度又增加1了,相应长度的最小末尾即nums【i】。
最后整个数组的LIS就dp的长度。(因为这个长度下最小末尾存在,所以一定存在LIS。LIS长度和最小末尾是同步增加的。)
AC代码:
class Solution {
public:
/**
* @param nums: An integer array
* @return: The length of LIS (longest increasing subsequence)
*/
int longestIncreasingSubsequence(vector<int> &nums) {
// write your code here
int size=nums.size();
if (size<=0)
{
return 0;
}
vector<int> dp;
for (int i=0;i<size;i++)
{
if (dp.empty()||nums[i]>dp.back())
{
dp.push_back(nums[i]);
continue;
}
//二分查找;
int l=0,r=dp.size()-1,mid;
while(l<=r)
{
mid=(l+r)/2;
if (dp[mid]<nums[i])
{
l=mid+1;
}
else
{
r=mid-1;
}
}
dp[l]=nums[i];
}
return dp.size();
}
};
啰嗦几句为何以上二分法查找法最终结果是L。
首先要明确,最后一次循环时mid左边的元素小于(等于)目标,mid右边的元素大于(等于)目标,而mid对应的元素值则无法确定,且mid本身等于L。
循环结束时一定有 L>R,有两种可能,一种是上次循环L=mid+1,另一种是R=mid-1。
第一种,说明最后一次循环时dp【mid】 < nums【i】,所以应返回mid+1,即最终的L;
第二种,说明最后一次循环时dp【mid】> nums【i】,所以应返回mid,即最终的L;
终上,第一个大于等于目标的位置是循环结束后的L。
PS:还可以使用lower_bound函数,其基本用途是查找有序区间中第一个大于或等于某给定值的元素的位置,内部实现基于二分法。
代码为:
int longestIncreasingSubsequence(vector<int> &nums)
{
// write your code here
int size=nums.size();
if (size<=0)
{
return 0;
}
vector<int> dp;
for (int i=0;i<size;i++)
{
if (dp.empty()||nums[i]>dp.back())
{
dp.push_back(nums[i]);
continue;
}
int ind=lower_bound(dp.begin(),dp.end(),nums[i])-dp.begin();
dp[ind]=nums[i];
}
return dp.size();
}
—————————————————分割线,原始错误想法记录————————————————
最开始的想法是遍历数组,将当前元素nums【i】作为子序列起始值。然后遍历 i 之后的元素,如果比起始值大,长度就+1,同时用该元素更新起始值,进入下一次对比。最后返回子序列长度最大的。
这个思路是错的,因为无法保证每次求得的长度是当前位置(i)上最长子序列。
如【10,11,1,12,2,11,3,10,4】,返回结果是3,而实际结果是4。
代码:
int longestIncreasingSubsequence1(vector<int> &nums)
{
int size=nums.size();
if (size<=0)
{
return 0;
}
int pre;
int len=1,result=1;
for (int i=0;i<size;i++)
{
pre=nums[i];
len=1;
for (int j=i+1;j<size;j++)
{
if (nums[j]>pre)
{
pre=nums[j];
len++;
}
}
if (len>result)
{
result=len;
}
}
return result;
}