『LIS问题的优化:O(NlogN)效率的算法』

版权声明:随你转载,你开心就好(记得评论和注明出处哦) https://blog.csdn.net/Prasnip_/article/details/81118951

<更新提示>

<第一次更新>深究LIS问题


<正文>

LIS问题

求解长度为n的序列中最长上升子序列的长度。

分析

显然可以令f[i]为以元素a[i]结尾的最长上升子序列的长度,则状态转移方程为: f [ i ] = m a x { f [ j ] + 1 } ( f [ j ] < f [ i ] ) ,可以直接花 O ( n 2 2 ) 的时间来暴力转移。其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,a[1000080]={},f[100080]={};
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],f[i]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
        }
    }
    cout<<f[n]<<endl;
    return 0;
}

这是LIS问题的暴力解法,详见『基础DP专题:LIS,LCS和背包九讲(不包括泛化物品)及实现』。这里不在深入讨论。

那么我们考虑一个升级版的LIS问题:

LIS模板题

题目描述
有N个整数,输出这N个整数的最长上升序列、最长下降序列、最长不上升序列和最长不下降序列。
输入格式
第一行,仅有一个数N。 N<=700000 第二行,有N个整数。 -10^9<=每个数<=10^9
输出格式
第一行,输出最长上升序列长度。 第二行,输出最长下降序列长度。 第三行,输出最长不上升序列长度。 第四行,输出最长不下降序列长度。
样例数据
input
10
1 3 0 8 6 2 3 1 4 2
output
4
4
4
4
时间限制:
1s
空间限制:
256MB

分析

70万的数据量, O ( n 2 2 ) 的效率一遍都不够,还要做四遍。显然,我们必须更换策略。
O ( n ) 的算法显然难以实现,如果用高级数据结构优化,实现难度甚至不在noip提高组范围内,永不考虑。
那我们考虑 O ( n l o g 2 n ) 的算法, l o g 2 700000 = 19.417 ,所以 O ( n l o g 2 n ) 的效率做4遍近似于 O ( 700000 19.417 4 ) = O ( 13591900 4 ) O ( 5.4 10 7 ) 在考虑范围内。我们尝试寻找该效率的算法。
怎么实现呢?我们考虑一个数组d[i],代表长度为i的最长上升子序列的最小尾元素。仔细理解d[i]的含义。那么我们如何完成这个数组呢?花费 O ( n ) 时间线性遍历序列。如果遇到一个数大于d数组末尾的数,说明它可以成为最长上升子序列的一部分,将d数组尾指针加一,存下这个数。如果这个数小于等于d数组末尾的数,那么我们在d数组中找到第一个大于等于它的数所对应的位置,并将当前这个数替换掉该位置的数。为什么要替换呢,首先是操作的合法性,既然d数组的尾指针所指的数大于等于它,那么它就可以插入到以前最长上升子序列的某个位置,那么插入到第一个大于等于它的位置即可。这样做是为了更好的响应以后的该操作及第一种情况,也就是说,这样替换,它的潜力就更大了,就有可能组成更长的最长上升子序列。完成d数组后,它的尾指针的大小就是最长上升子序列的长度。
那么为什么时间复杂度是 O ( n l o g 2 n ) 呢,显然,线性扫描时间复杂度 O ( n ) ,那么由第一种情况及定义可知,d数组是单调上升的,那么第二种情况查找第一个大于等于它的数字就可以用二分查找优化。所以总的时间复杂度就是 O ( n l o g 2 n )
再来考虑一个细节问题,为什么要查找第一个大于等于它是数而不是查找第一个大于它的数呢?原因是我们在有相等的数时,要先替换掉相等的数,不然就成了最长不下降子序列的做法
关于二分查找,我们可以手写,当然,可以直接用lower_bound函数,刚好符合我们的需求。但是,避免返回地址出错,我们从0开始存数就能避免。

最长上升子序列代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,t=0,a[100080]={},d[100080]={};
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    d[0]=a[0];
    for(int i=0;i<n;i++)
    {
        if(a[i]>d[t])d[++t]=a[i];
        else d[ lower_bound(d,d+t,a[i])-d ]=a[i];
    }
    cout<<t+1<<endl;
    return 0;
}

那么最长不下降子序列的d[i]就是代表长度为i的最长不下降子序列的最小尾元素,我们将比较d数组末尾元素与a[i]大小时将符号改为>=即可。由于最长不下降子序列中允许大小相同的元素存在,我们在查找时如有相同不必替换,需要查找第一个比它大的元素才能更优,即使用upper_bound函数即可。

最长不下降子序列代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,t=0,a[100080]={},d[100080]={};
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    d[0]=a[0];
    for(int i=0;i<n;i++)
    {
        if(a[i]>=d[t])d[++t]=a[i];
        else d[ upper_bound(d,d+t,a[i])-d ]=a[i];
    }
    cout<<t+1<<endl;
    return 0;
}

由此可以推出,最长下降子序列也是相同的。最长下降子序列的d[i]代表代表长度为i的最长下降子序列的最大尾元素。每一次扫描一个数,如果比d数组尾元素小就加入d数组,比d数组尾元素大或相等就在单调下降的d数组中找到第一个小于等于它的数,替换它,原理与最长上升子序列相同。不过,这个二分就要我们手写了,难度不是很大,如果对二分的细节处理还有疑惑,可以看我的博客『二分查找和二分答案』
最长下降子序列代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,t=0,a[100080]={},d[100080]={};
inline int find(int p[],int len,int num)
{
    int l=0,r=len;
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(p[mid]<num)r=mid;
        else l=mid;
    }
    if(p[l]>num)return r;
    else return l;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    d[0]=a[0];
    for(int i=0;i<n;i++)
    {
        if(a[i]<d[t])d[++t]=a[i];
        else d[ find(d,t,a[i]) ]=a[i];
    }
    cout<<t+1<<endl;
    return 0;
}

最长不上升子序列的代码就与最长下降子序列相近了。最长不上升子序列的d[i]代表代表长度为i的最长不上升子序列的最大尾元素。在扫描时,如果a[i]小于等于d数组尾元素,将其加入d数组,否则找到第一个小于它的数,因为可以相等,所以找比它小的才能更优,与最长不下降子序列相同。而在代码上把最长下降子序列的比较的符号改为小于等于,在二分查找时也把最后的比较改为大于等于即可。
最长不上升子序列代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,t=0,a[100080]={},d[100080]={};
inline int find(int p[],int len,int num)
{
    int l=0,r=len;
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(p[mid]<num)r=mid;
        else l=mid;
    }
    if(p[l]>=num)return r;
    else return l;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    d[0]=a[0];
    for(int i=0;i<n;i++)
    {
        if(a[i]<=d[t])d[++t]=a[i];
        else d[ find(d,t,a[i]) ]=a[i];
    }
    cout<<t+1<<endl;
    return 0;
}

那么整合起来就是LIS模板题的标准答案:

#include<bits/stdc++.h>
using namespace std;
int n,a[700080]={},t1=0,t2=0,t3=0,t4=0;
int d1[700080]={},d2[700080]={},d3[700080]={},d4[700080]={};
inline int find1(int p[],int len,int num)
{
    int l=0,r=len;
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(p[mid]<num)r=mid;
        else l=mid;
    }
    if(p[l]>num)return r;
    else return l;
}
inline int find2(int p[],int len,int num)
{
    int l=0,r=len;
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(p[mid]<num)r=mid;
        else l=mid;
    }
    if(p[l]>=num)return r;
    else return l;
}
inline void a_()
{
    d1[0]=a[0];
    for(int i=1;i<n;i++)
    {
        if(a[i]>d1[t1])d1[++t1]=a[i];
        else d1[ lower_bound(d1,d1+t1,a[i])-d1 ]=a[i];
    }
    cout<<t1+1<<endl;
}
inline void b_()
{
    d2[0]=a[0];
    for(int i=1;i<n;i++)
    {
        if(a[i]<d2[t2])d2[++t2]=a[i];
        else d2[ find1(d2,t2,a[i]) ]=a[i];
    }
    cout<<t2+1<<endl;
} 
inline void c_()
{
    d3[0]=a[0];
    for(int i=1;i<n;i++)
    {
        if(a[i]<=d3[t3])d3[++t3]=a[i];
        else d3[ find2(d3,t3,a[i]) ]=a[i];
    }
    cout<<t3+1<<endl;
} 
inline void d_()
{
    d4[0]=a[0];
    for(int i=1;i<n;i++)
    {
        if(a[i]>=d4[t4])d4[++t4]=a[i];
        else d4[ upper_bound(d4,d4+t4,a[i])-d4 ]=a[i];
    }
    cout<<t4+1<<endl;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    a_();b_();c_();d_();
}

<后记>


<废话>

猜你喜欢

转载自blog.csdn.net/Prasnip_/article/details/81118951
今日推荐