LIS(最长上升子序列)的三种经典求法

求最长上升子序列的三种经典方案:

给定一个长度为 \(N\) 的数列,求它数值单调递增的子序列长度最大为多少。即已知有数列 \(A\)\(A=\{A_1,A_2....A_n\}\) ,求 \(A\) 的任意子序列 \(B\)\(B=\{A_{k_1},A_{k_2}....A_{k_p}\}\) ),使 \(B\) 满足 \(k_1<k_2<....<k_p\)\(B=\{A_{k_1}<A_{k_2}<....<A_{k_p}\}\) 。求 \(p\) 的最大值。



\(solution\quad 1:\)

先说一种最普遍的方法,因为这道题很容易想到一种动态规划。我们要求最长上升子序列,而如果我们要将这个问题分阶段,那么我们肯定要知道我们当前阶段最后一个元素为多少,还有当前我们的序列有多长。我们可以用前者来充当第一维描述:设 \(F[i]\) 表示以 \(A[i]\) 为结尾的最长上身子序列的长度,而我们肯定只能从 \(i\) 前面的且末尾元素比 \(A[i]\) 小的状态转移过来:

\(F[i]=^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]+1\}\)

初始值为 \(F[0]=0\) ,而答案可以是任何阶段中只要长度最长的那一个,所以我们边转移边统计答案。

复杂度: \(O(n^2)\)

\(code\quad 1:\)

#include<iostream>
#include<cstdio>
#define ll long long
#define rg register int
using namespace std;

int n,ans;
int a[10005];
int f[10005];

int main(){ cin>>n;
    for(rg i=1;i<=n;++i) cin>>a[i];
    for(rg i=1;i<=n;++i){
        for(rg j=1;j<i;++j)
            if(a[j]<a[i])f[i]=max(f[i],f[j]);
        ++f[i]; ans=max(ans,f[i]);
    }cout<<ans<<endl;
    return 0;
}

\(solution\quad 2:\)

然后我们先对上一种方法进行优化,我们将转移方程列一下:

\(F[i]=^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]+1\}\)

我们发现大括号中的1与 \(j\) 似乎没有半毛钱关系,所以我们将它提取出来:

\(F[i]=^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]\}+1\)

然后我们发现我们只需要将比 \(i\) 小的所有的符合 \(A[j]<A[i]\)\(F[j]\) 的最大值求出来,但是这个条件 \(A[j]<A[i]\) 实在是太麻烦了,所以我们换一种方法:我们将 \(A\) 数组的每一个元素先记下他现在的标号,然后按照权值从小到大排序,接着我们按从大到小的顺序枚举 \(A\) 数组,这时我们需要从之前的标号比它小的状态转移过来,这个我们只需要建立一个与编号为下标维护长度的最大值的树桩数组即可,枚举 \(A\) 数组时按元素的序号找到它之前序号比他小的长度最大的状态更新,然后将它也加入树状数组中。 复杂度: \(O(nlog(n))\)

\(code\quad 2:\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
#define rg register int
using namespace std;

int n;
int s[200005];

struct su{
    int v,id;
    inline bool operator <(su x){return v<x.v;}
}a[200005];

inline void add(int x,int y){
    for(;x<=n;x+=x&-x) s[x]=max(s[x],y);
}

inline int ask(int x){
    rg res=0;
    for(;x>=1;x-=x&-x) res=max(s[x],res);
    return res;
}

int main(){
    cin>>n;
    for(rg i=1;i<=n;++i)
        cin>>a[i].v,a[i].id=i;
    sort(a+1,a+n+1);
    for(rg i=1;i<=n;++i)
        if(i>1&&a[i].v!=a[i-1].v)// 这一步是去重,如果没有这个if就是求最长不下降子序列!
            add(a[i].id,ask(a[i].id)+1);
    cout<<ask(n)<<endl;
    return 0;
}

\(solution\quad 3:\)

这是最快的方法:贪心加二分查找

我们之前说过:我们肯定要知道我们当前阶段最后一个元素为多少,还有当前我们的序列有多长。前两种方法都是用前者做状态,我们为什么不可以用后做状态呢?:设 \(F[i]\) 表示长度为 \(i\) 的最长上升子序列的末尾元素的最小值,我们发现这个数组的权值一定单调不降(仔细想一想,这就是我们贪心的来由)。于是我们按顺序枚举数组 $ A$ ,每一次对 \(F[]\) 数组二分查找,找到小于 \(A[i]\) 的最大的 $F[j] $ ,并用它将 \(F[j+1]\)更新。

复杂度: \(O(nlogn)\)

\(code\quad 3:\)

#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
#define rg register int
using namespace std;

int n;
int a[200005];
int f[200005];

int main(){
    cin>>n;
    for(rg i=1;i<=n;++i) cin>>a[i];
    rg ans=1; f[1]=a[1];
    for(rg i=2;i<=n;++i){
        rg l=1,r=ans,mid;
        while(l<=r){
            mid=(l+r)>>1;
            if(a[i]<=f[mid])r=mid-1;
            else l=mid+1;
        }f[l]=a[i];
        if(l>ans)++ans;
    }cout<<ans<<endl;
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/812-xiao-wen/p/10992613.html