目录
题目:
给定一个长度为n(0<n<=10^5)的顺序存储的线性表,请设计一个算法查找该线性表中第一个出现的最长的递增子序列。
例如(n=5 ,nums=[1,3,2,4,5])输出:1 3 4 5
【解释】最长递增子序列一共有两个,分别是1 3 4 5和1 2 4 5,但是1 3 4 5比1 2 4 5先出现,所以输出1 3 4 5
【注】本题来自于我自己在做一个简单题(就是把本文的题目中的最长递增子序列改为最长连续递增子序列)时偶然得出的。
废话不多说,直接开干!!~
我们知道如果只是求一个数组的最长递增子序列的个数或者长度的话可以有动态规划o(n^2)和动态规划+二分查找o(n*logn)两种解法(如果还不了解这两种解法的可先去了解一下),那么基于这两种方法,我们也可以衍生出解决本题的两种对应的解法(o(n^2)和o(n*logn))。
【注】本文以nums=[1, 3, 2, 4, 5]这个数字序列来进行解释
o(n^2)解最长递增子序列的长度问题
首先,我们来看看这o(n^2)的解法:
在求最长递增子序列的个数的题目中我们使用到了一个一维辅助数组dp,每一个dp[i]都表示为如果以nums[i]作为子序列的末尾的话最长的递增子序列是多长。【如下】
nums[0] :1 | nums[1]:3 | nums[2]:2 | nums[3]:4 | nums[4]:5 |
1 | 2 | 2 | 3 | 4 |
对于每一个dp[i]我们都是通过遍历nums[j=0...i-1]和dp[j=0...i-1], 去寻找所有的满足nums[j]<nums[i]条件下dp[j]的最大值,例如dp[2],对应的nums[2]为2,它前面的数字序列为1 3,满足小于nums[2]的只有nums[0],nums[1]大于nums[2],所以满足小于条件中的dp[j]中的最大值就是dp[0]=1,所以dp[2]=dp[0]+1(自身)=2。
o(n^2)解最长递增子序列的个数问题
另外,我们还是用到了一个一维计数数组cnt,每一个cnt[i]表示为如果硬是要以nums[i]为子序列的结尾的话最长递增子序列的个数有多少个。【如下】
nums[0]: 1 | nums[1]: 3 | nums[2]: 2 | nums[3]: 4 | nums[4]: 5 |
1 | 1 | 1 | 2 | 2 |
在更新每一个cnt[i]之前先更新dp[i]的值,在得到dp[i]的值后我们遍历一遍dp[j=0...i-1],寻找满足条件 nums[j]<nums[i]&&dp[j]+1==dp[i] 的所有的cnt[j=x0,x1,x...],那么我们的cnt[i]=cnt[j=x0]+cnt[j=x1]+....(累加和)。
接下来呢,通过一个for循环我们就能将所有的dp[i],和cnt[i]的值全部填入,然后我们再遍历一遍dp数组,将dp数组中的最大值记录下来maxLength,那这个最大值maxLength呢自然就是我们这个数组的最长递增子序列的长度了,接下来我们又再遍历一遍cnt数组,int sum=0,对于每一个cnt[i],如果dp[i]=maxLength呢,那就把它加入到sum中,即sum+=cnt[j],那最终的sum呢就是我们最长递增子序列的个数了。至于这里为什么会是这样的呢,我们可以从cnt[i]和dp[i]的含义来理解(dp[i]:以第i+1个元素结尾时的最长递增子序列的长度。。。。cnt[i]:以i+1个元素为结尾的最长递增子序列的个数)。所以了,这样下来我们的最长递增子序列的长度了就是dp数组里面的最大值4,
最长递增子序列的个数呢就是:maxLength对应的下标只有4,最长递增子序列的额个数就是cnt[4]=2。(是不是和我们的1,3,4,5 , 1,2,4,5)一样的了)
o(n^2)解第一个出现的最长递增子序列的问题
解决了最长递增子序列的长度和个数,那么o(n^2)解决出现的第一个最长递增子序列是什么的问题又要怎么弄呢,哎,接下来且看我们继续操作。。。。
在解决这个问题中的时候我们就不需要cnt数组了,但dp数组保持不变。现在我们来重新开一个preIndex数组,见名思义,这个数组中对应每一个preIndex[i]表示为在j=0...i-1范围上满足nums[j]<nums[i]&&dp[j]+1==dp[i]条件的j的最小值(这样即可便是表示出最早出现的条件)。对了,我们还要处理一下dp[i]==1的时候的情况,这种情况下没有上一个下标了,所以dp[i]==1的时候,我们让preIndex[i]=-1,表示无上一值。【如下】
nums[0]: 1 | nums[1]: 3 | nums[2]: 2 | nums[3]: 4 | nums[4]: 5 |
-1 | 0 | 0 | 1 | 3 |
nums[0] :1 | nums[1]:3 | nums[2]:2 | nums[3]:4 | nums[4]:5 |
1 | 2 | 2 | 3 | 4 |
就拿preIndex[3]来说事吧,dp[3]=3,满足nums[j]<nums[i]&&dp[j]+1==dp[i]这个条件的有j=1和j=2,那么我们的preIndex=min(j=1,j=2)=1,这样当我们要输出第一个出现的最长递增子序列的时候当我们来到nums[3]位置的时候是不是就可以通过preIndex[3]来直接得到上一个位置的下标了,就不会选择nums[2]了。
更新完dp和preIndex的全部值,接下来就是如何根据这写信息来输出第一个出现的最长递增子序列的了。首先我们遍历dp数组,找到第一次出现的最大值的位置并记录下来。
int maxL=0,ansIndex=-1;
for(int i=0;i<len;i++) //len为dp数组的长度
{
if(maxL<dp[i])
{
maxL=dp[i];
ansIndex=i;
}
}
接着我们就可以根据这个ansIndex来依次找到我们想要的了
int *ans=new int[maxLength],tail=maxLength-1;
//maxLength为上文提到的最长递增子序列的长度
for(int i=ansIndex;i!=-1;i=preIndex[i])
{
ans[tail--]=nums[i]
}
这样下来了,那最后ans数组存储不就是第一个出现的最长递增子序列了嘛。
o(n*logn)解最长递增子序列的长度问题
看完上文我们知道,我们每次更新dp[i]的值时候都是通过遍历前面的dp[j=0..i-1],每一都需要寻找满足nums[j]<nums[i]条件中的dp[j]的最大值,而在o(n*logn)的解法中,我们不这样做遍历,我们使用一个新的theBest数组,对于每一个theBest[i]都表示为:当前更新dp到了k(k>=i)位置,前面的子数组中所有长度为i的子序列的最后一个数字的最优解是多少(这里最优解即为最小值,如1 3 6和1 3 4序列那么theBest[3]=4,这样的话如果后面遇上了一个数字5,那么我们查询到theBest[3]时,发现前面的序列中所有长度为3的序列的最后一个数字最小可以是4,那么我的5一定可以接在这个序列的后面组成一个长度为4的递增子序列)
nums[0] :1 | nums[1]:3 | nums[2]:2 | nums[3]:4 | nums[4]:5 |
1 | 2 | 2 | 3 | 4 |
0 | 1 | 2 | 3 | 4 |
Nan | 1 |
0 | 1 | 2 | 3 | 4 |
Nan | 1 | 3 |
0 | 1 | 2 | 3 | 4 |
Nan | 1 | 2 |
0 | 1 | 2 | 3 | 4 |
Nan | 1 | 2 | 4 |
0 | 1 | 2 | 3 | 4 |
Nan | 1 | 2 | 4 | 5 |
从表格中我们可以看出我们可以看出当k=1时,截止到当前位置,前面能形成的长度为2的、递增子序列就只有1 3序列,所有我们theBest[2]=3,而当我们的k=2时,我们的能形成的长度为2的递增子序列有1 3和1 2 两个序列,但是2<3,所以我们更新theBest[2]的值为2。
那我们每一次到了一个新的位置后是如何选择更新哪一个theBest的值的呢,总不能又是遍历吧!当然不是,下面我们来看看序列:1 4 3 6 5 7 7 8 的情况
0 | 1 | 2 | 3 | 4 | 5 |
Nan | 1 | 3 | 5 | 7 | 8 |
根据上面填写的规则,我们很容易得到这个theBest的情况,加入这时我们数字序列1 4 3 6 5 7 7 8 的后面还有一个6的时候我们要怎么填呢?不难发现,我们的theBest数组中的值总是保持着单调递增的姿势,所以我们就可以通过二分查找的方法找到从左到右第一个大于或者等于当前值curValue的theBest[i], 例如这里我们的curValue=6,那么第一个大于或者等于6的数就是theBest[i=4]=7,你看theBest[i-1=3]的值是不是小于6的啊,是不是就意味着前面的序列中有长度为3的递增子序列并且最后一个数字为theBest[i-1=3]=5啊,那我们是不是就可以认为这个6一定可以接着这个子序列的后面形成一条长度为4的递增子序列啊,而且还使得这条新的长度为4的子序列是截止到当前位置最好的一条子序列啊。【代码如下】
int left=1,right=length_of_theBest+1,mid;
//length_of_theBest是theBest数组的当前长度,也可以认为是截止到当前的上一个位置,最长递增子序列
//的长度,right=legth_of_theBest+1而不是right=length_of_theBest,主要是为了照顾如果theBest数
//组中的值全部都小于curValue的话,那就要在theBest最右边的一个值的下一个位
//置theBest[length_of_theBest+1]=curValue,表示出现了一条最长的递增子序列了
while(left<right)
{
mid=(left+right)>>1; //(left+right)/2
if(curValue==theBest[mid])
{
left=mid;
break;
}
else if(curValue>theBest[mid]) left=mid+1;
else right=mid;
}
theBest[left]=curValue;
那以后遍历完nums后,theBest数组的有效长度可不就是最长递增子序列的长度了嘛!!!
o(n*logn)解最长递增子序列的个数
这个问题叙述起来就比较麻烦了,建议看看leetcode的题解!!!
链接:力扣
o(n*logn)解最先出现的最长递增子序列
这个问题是我自己想出来的嘛,所有还得我自己来填这个坑!
这个问题的解法思路来自于o(n*logn)解最长递增子序列的个数问题的解法,这里我们定义一个结构体
struct data
{
int val; //对应的nums[selfIndex]的值
int selfIndex; //这个值在nums数组中的对应的下标
int preIndex; //val参与构成的递增子序列中最早出现的那个子序列在val前面的那个值的下标
};
那么在这里我们每次更新二维数组theBest的值的时候先找到应该放在哪个一维子数组里面,这里认为是theBest[i],然后再去theBest[i-1]数组中去寻找出现最早的&&theBest[i][j].val<curValue.val,找到后让curValue.preIndex=theBest[i-1][j].selfIndex,这样不就链接起来了嘛。
那要怎么寻找theBest[i-1][j]呢,别急,我们看看下面这个表,示范数字序列:1 9 4 3 7 8 5
1 | 2 | 3 | 4 | 5 | |
0 | 1:0:-1 | 9:1:0 | 7: 4: 2 | 8: 5: 4 | |
1 | 4:2:0 | 5: 6:2 | |||
2 | 3: 3: 0 |
假设当前在更新数字5,
哎,我们发现,theBest数组不仅每一列的数字val有序,并且出现的时间(下标selfIndex)也是有序的,这就意味我们只需要二分找到theBest[i-1]中从上往下第一个小于curValue.val值的元素就好了,例如这里curValue.val=5,应该放在theBest[3]中,theBest[3-1]中第一个小于5的就是数字theBest[2][1]=4,所以我们curValue.preIndex=theBest[2][1].selfIndex.这样不就链接起来了嘛
附上完整代码
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
struct data
{
int val,selfIndex,preIndex;
}node[100001];
void print(vector<vector<struct data> >& ans)
{
int len=ans.size();
int *dp=new int[len],tail=len-1;
for(int i=ans[len-1][0].selfIndex;i!=-1;i=node[i].preIndex)
{
dp[tail--]=node[i].val;
}
for(int i=0;i<len;i++) cout<<dp[i]<<" ";
delete [] dp;
}
void addNums(vector<vector<data> >& ans,struct data& num)
{
int left=0,right=ans.size(),mid;
while(left<right)
{
mid=(left+right)>>1;
if(ans[mid].back().val==num.val)
{
left=mid;
break;
}
else if(ans[mid].back().val<num.val) left=mid+1;
else right=mid;
}
if(left==0)
{
ans[0].push_back(num);
}
else
{
int L=0,R=ans[left-1].size()-1;
while(L<R)
{
mid=(L+R)>>1;
if(ans[left-1][mid].val<num.val) R=mid;
else L=mid+1;
}
num.preIndex=ans[left-1][L].selfIndex;
if(left==ans.size())
{
vector<struct data> temp;
temp.push_back(num);
ans.push_back(temp);
}
else ans[left].push_back(num);
}
}
void theFirstLis(vector<vector<struct data> >& ans,int len)
{
if(len==1)
{
cout<<node[0].val;
return;
}
vector<struct data> t;
t.push_back(node[0]);
ans.push_back(t);
for(int i=1;i<len;i++)
{
addNums(ans,node[i]);
}
print(ans);
}
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>node[i].val;
node[i].preIndex=-1;
node[i].selfIndex=i;
}
vector<vector<struct data> > ans;
theFirstLis(ans,n);
return 0;
}