最长不下降子序列LIS

最长不下降子序列问题

最长上升子序列问题是解决很多问题的根本,它能帮助你理解二分的思想。

引言

考虑一下:对于一个序列 nn ,请你查找nn中最长的子序列aa,使得任意 i<ji<ja[i]<=a[i]a[i]<=a[i].

例如一个长度为55nn=55 33 11 22 44;
显然,它的最长不下降子序列就是 11 22 44.
我们可以想一下自己是如何看出它的最长不下降子序列的.
首先,第一个数是55,前面没有数,所以它可以是子序列的一部分,那么我们可以将它放到考虑的第一位:
55 ? ? ? ?
因为放了一个数,所以答案要加一:
ans=1ans=1
第二个数是33,有两种选择,一种是插入到刚插入的数的后面,第二种就是替换掉55.因为5>35>3,所以33不可以放到55的后面去.5533大还在答案中,那我要你55有什么用?果断替换:
33 ? ? ? ?
ans=1ans=1
第三个数是11,与33同理,替换:
11 ? ? ? ?
ans=1ans=1
第四个数是22,比刚插入的数小,可以插入,那么就变为:
11 22 ? ? ?
ans=2ans=2
第五个数同理插入:
11 22 44 ? ?
ans=3ans=3
至此,我们的大脑(?)(?)处理完了这样一个长度为55的最长不下降子序列,当长度很小时我们能顺利解决,剩下的就交给计算机啦.

二分求解

在我们模拟的时候,有这样一个操作,当新读入的数字小于答案数列的第ansans个数的时候,我们需要找到要用读入数字替换掉的位置.这个时候,选择从第11个数挨个比对到第ansans个数就很睿智 ,于是我们选择二分求解.
看这样一个序列:
11 22 44 66 88 1010
假设新读入的数是3.
那么我们取这个元素个数为66的序列的中间数: 44
4433大,而这个序列是单调递增的,要替换的位置一定在左侧.我们取左侧的序列.
11 22 44
中间数为22, 2<32<3,那么我们取右侧的序列
44
可以判断,序列中只有一个元素,要替换的数就决定是它啦!
这样子寻找就省去了O(N)O(N)的复杂度转为OO((loglogn))了,为我们节省了很多时间.
代码实现

#include <cstdio>
#include <iostream>
#define maxn 100001
#define minn -99999
using namespace std;
int n,a[maxn],f[maxn],ans;//f数组就相当于上面的答案数组,a数组存的是数的值.
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    f[0]=minn;
    for(int i=1;i<=n;i++){
        if(a[i]>f[ans]){//直接向答案序列后加数
            f[ans+1]=a[i];
            ans++;
        }
        else{
            int l=0,r=ans;
            while(l<r){
                int mid=(l+r)>>1;
                if(f[mid]>a[i]){
                    r=mid;//如果中间数比要加的数大,那么就要取左侧序列
                }
                else{
                    l=mid+1;//如果中间数比要加的数小,那么就要取右侧序列
                }
            }
            f[l]=a[i];//最后替换
        }
    }
    printf("%d\n",ans);
    return 0;
}

更简洁的写法

你可能会问了:有没有更简洁的写法来实现这样高效的二分呢?答案是有的.在我们的c++ STL库中就有这样的函数,来帮助我们实现查找.
在< algorithm > 库中,有个叫lower_bound的函数,先看他的函数定义:

template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );

它的功能是:
返回指向范围 [first, last) 中首个不小于(即大于或等于) value 的元素的迭代器,或若找不到这种元素则返回 last 。

如果想了解更多关于lower_bound的严格说明,请点击传送门
那么我们有了这个工具之后就可以这样改进我们的程序,让它更简洁.

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define maxn 100001
using namespace std;
int n,a[maxn],f[maxn],ans;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    memset(f,10000,sizeof f);//要初始化的大,后面的比较就不会因为值为0出错
    f[0]=-1;//vis[0]除外,它要设为-1
    for(int i=1;i<=n;i++){
        int v=lower_bound(f,f+1+ans,a[i])-f;//在1到ans的区间中寻找第一个比a[i]大的.
        ans=max(ans,v);//更新答案
        f[v]=min(f[v],a[i]);//替换掉比a[i]大的
    }
    printf("%d\n",ans);
    return 0;
} 
 

这样就将二分的过程运用STL函数省去了,提高了我们的编程效率.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章