最长上升子序列
最长上升子序列
1、\(n^2\) 做法
首先我们要知道,对于每一个元素来说,最长上升子序列就是其本身。那我们便可以维护一个 \(dp\) 数组,使得 \(dp[i]\) 表示以第 \(i\) 元素为结尾的最长上升子序列长度,那么对于每一个 \(dp[i]\) 而言,初始值即为 1;
那么 \(dp\) 数组怎么求呢?我们可以对于每一个 \(i\),枚举在 \(i\) 之前的每一个元素 \(j\),然后对于每一个 \(dp[j]\),如果元素 \(i\) 大于元素 \(j\),那么就可以考虑继承,而最优解的得出则是依靠对于每一个继承而来的 \(dp\) 值,取 \(max\)
for(int i=1;i<=n;i++) {
dp[i]=1;//初始化
for(int j=1;j<i;j++){//枚举i之前的每一个j
//用if判断是否可以拼凑成上升子序列,如果是,则更新最优状态
if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
}
}
最后,因为我们对于 \(dp\) 数组的定义是到 \(i\) 为止的最长上升子序列长度,所以我们最后对于整个序列,只需要输出 \(dp[n]\) 即可。
从这个题我们也不难看出,状态转移方程可以如此定义:
下一状态最优值=最优比较函数(已经记录的最优值,可以由先前状态得出的最优值)
——即动态规划具有 判断性继承思想
2、\(nlogn\) 做法
我们其实不难看出,对于 \(n^2\) 做法而言,其实就是暴力枚举:将每个状态都分别比较一遍。但其实有些没有必要的状态的枚举,导致浪费许多时间,当元素个数到了 \(10^4-10^5\) 以上时,就已经超时了。而此时,我们可以通过另一种动态规划的方式来降低时间复杂度:
将原来的 \(dp\) 数组的存储由数值换成该序列中,上升子序列长度为 \(i\) 的上升子序列,的最小末尾数值
这其实就是一种几近贪心的思想:我们当前的上升子序列长度如果已经确定,那么如果这种长度的子序列的结尾元素越小,后面的元素就可以更方便地加入到这条我们臆测的、可作为结果的上升子序列中。
#include<bits/stdc++.h>
using namespace std;
const int N=300010;
int a[N],n,m,ans;
int dp[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
memset(dp,0x3f,sizeof(dp));
//初始值要设为INF
/*原因很简单,每遇到一个新的元素时,就跟已经记录的f数组当前所记录的最长
上升子序列的末尾元素相比较:如果小于此元素,那么就不断向前找,直到找到
一个刚好比它大的元素,替换;反之如果大于,么填到末尾元素的下一个q,INF
就是为了方便向后替换啊!*/
dp[1]=a[1];
ans=1;//通过记录dp数组的有效位数,求得个数
/*因为上文中所提到我们有可能要不断向前寻找,
所以可以采用二分查找的策略,这便是将时间复杂
度降成nlogn级别的关键因素。*/
for(int i=2;i<=n;i++){
//如果刚好大于末尾,暂时向后顺次填充
if(a[i]>dp[ans]) {dp[++ans]=a[i];continue;}
int l=1,r=ans,res=0;
while(l<=r){
int mid=(l+r)>>1;
if(dp[mid]<a[i]){
res=mid;
l=mid+1;
}else{ /*如果仍然小于之前所记录的最小末尾,那么不断
向前寻找(因为是最长上升子序列,所以dp数组必
然满足单调)*/
r=mid-1;
}
}
dp[res+1]=min(dp[res+1],a[i]);//更新最小末尾
}
cout<<ans;
return 0;
}