最长不下降子序列LIS

最长不下降子序列问题

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

引言

考虑一下:对于一个序列 n n n ,请你查找 n n n中最长的子序列 a a a,使得任意 i &lt; j i&lt;j i<j a [ i ] &lt; = a [ i ] a[i]&lt;=a[i] a[i]<=a[i].

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

二分求解

在我们模拟的时候,有这样一个操作,当新读入的数字小于答案数列的第 a n s ans ans个数的时候,我们需要找到要用读入数字替换掉的位置.这个时候,选择从第 1 1 1个数挨个比对到第 a n s ans ans个数就很睿智 ,于是我们选择二分求解.
看这样一个序列:
1 1 1 2 2 2 4 4 4 6 6 6 8 8 8 10 10 10
假设新读入的数是3.
那么我们取这个元素个数为 6 6 6的序列的中间数: 4 4 4
4 4 4 3 3 3大,而这个序列是单调递增的,要替换的位置一定在左侧.我们取左侧的序列.
1 1 1 2 2 2 4 4 4
中间数为 2 2 2, 2 &lt; 3 2&lt;3 2<3,那么我们取右侧的序列
4 4 4
可以判断,序列中只有一个元素,要替换的数就决定是它啦!
这样子寻找就省去了 O ( N ) O(N) O(N)的复杂度转为 O O O ( ( ( l o g log logn ) ) )了,为我们节省了很多时间.
代码实现

#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函数省去了,提高了我们的编程效率.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值