最长不降子序列nlogn写法

转自大佬lvmememe已经停更的博客https://www.cnblogs.com/itlqs/p/5743114.html
今天花了很长时间终于弄懂了这个算法……毕竟找一个好的讲解真的太难了,所以励志我要自己写一个好的讲解QAQ

这篇文章是在懂了这个问题n^2解决方案的基础上学习。

解决的问题:给定一个序列,求最长不下降子序列的长度(nlogn的算法没法求出具体的序列是什么)

定义:a[1…n]为原始序列,d[k]表示长度为k的不下降子序列末尾元素的最小值,len表示当前已知的最长子序列的长度。
初始化:d[1]=a[1]; len=1; (0个元素的时候特判一下)
现在我们已知最长的不下降子序列长度为1,末尾元素的最小值为a[1],那么我们让i从2到n循环,依次求出前i个元素的最长不下降子序列的长度,循环的时候我们只需要维护好d这个数组还有len就可以了。
关键问题就是怎么维护?
可以看出我们是要用logn的复杂度维护的。实际上利用了d数组的一个性质:单调性。(长度更长了,d[k]的值是不会减小的)
考虑新进来一个元素a[i]:
  如果这个元素大于等于d[len],直接让d[len+1]=a[i],然后len++。这个很好理解,当前最长的长度变成了len+1,而且d数组也添加了一个元素。
  如果这个元素小于d[len]呢?说明它不能接在最后一个后面了。那我们就看一下它该接在谁后面。
    准确的说,并不是接在谁后面。而是替换掉谁。因为它接在前面的谁后面都是没有意义的,再接也超不过最长的len,所以是替换掉别人。那么替换掉谁呢?就是替换掉那个最该被它替换的那个。也就是在d数组中第一个大于它的。第一个意味着前面的都小于等于它。假设第一个大于它的是d[j],说明d[1…j-1]都小于等于它,那么它完全可以接上d[j-1]然后生成一个长度为j的不下降子序列,而且这个子序列比当前的d[j]这个子序列更有潜力(因为这个数比d[j]小)。所以就替换掉它就行了,也就是d[j]=a[i]。其实这个位置也是它唯一能够替换的位置(前面的替了不满足d[k]最小值的定义,后面替换了不满足不下降序列)
  至于第一个大于它的怎么找……STL upper_bound。每次复杂度logn。

至此,我们就神奇的解决了这个问题。按照这个思路,如果需要求严格递增的子序列怎么办?
仍然考虑新进来一个元素a[i]:
  如果这个元素大于d[len],直接让d[len+1]=a[i],然后len++。这个很好理解,当前最长的长度变成了len+1,而且d数组也添加了一个元素。
  如果这个元素小于等于d[len]呢?说明它不能接在最后一个后面了。那我们就看一下它该接在谁后面。
    同样的道理,只是换成lower_bound即可。每次复杂度logn。
--------2018.4.14更新--------
很久没看,没想到这篇文章有这么多人阅读了,感觉最长递增子序列这里讲的不是太清楚,因此补充一下。
最长递增子序列和最长不下降子序列的不同之处在于,d数组的单调性更严格了:一定是单调递增的。
可以用反证法来证明这一点:假设有d[i]=d[i+1],也就是长度为i+1的子序列最后一位最小是d[i+1],那假设这个子序列是a[1], a[2], …, a[i+1],在这个子序列里面,a[i]<d[i+1]肯定成立,不然就不是最长递增子序列了。那也就是说a[i]<d[i]了,那a[1], a[2], …, a[i]就构成了一个长度为i的子序列,而且最后一个数比d[i]小。那d[i]肯定就不符合定义了。
那这个性质有什么意义呢?
仍然考虑新进来一个元素a[i]:
  如果这个元素大于d[len],直接让d[len+1]=a[i],然后len++。这个很好理解,当前最长的长度变成了len+1,而且d数组也添加了一个元素。
  如果这个元素等于d[len],那么可以保证d[1…len-1]都是小于a[i]的(根据上面的证明),因此这个元素就没有什么意义了,直接忽略就好,因为它无法接在任何一个元素d后面产生一个更有优势的子序列。
  如果这个元素小于d[len],那么就在d数组中找到第一个大于等于它的元素(这个元素必然存在,至少d[len]就是),把这个元素替换成a[i]即可。
  实际上可以发现,小于等于的时候可以统一按照lower_bound替换的方式来处理。
这样做肯定是对的,而邝斌的模板上实际上是求的最长不下降子序列,而不是最长上升子序列。不信可以测试一下"1 2 3 2 3 2"这个样例。切记,不要迷信权威,学会自己思考。


下面是最长不下降子序列的代码,注释里面说明了如何改成最长上升子序列。

子序列nlogn  Song 
       #include<cstdio>
    #include<algorithm>
    using namespace std;int a[40005];
    int d[40005];int main()
    {
        int n;
        scanf("%d",&n);
        for (int i=1;i<=n;i++) scanf("%d",&a[i]);
        if (n==0)  //0个元素特判一下 
        {
            printf("0\n");
            return 0;
        }
        d[1]=a[1];  //初始化 
        int len=1;
        for (int i=2;i<=n;i++)
        {
            if (a[i]>=d[len]) d[++len]=a[i];  //如果可以接在len后面就接上,如果是最长上升子序列,这里变成> 
            else  //否则就找一个最该替换的替换掉 
            {
                int j=upper_bound(d+1,d+len+1,a[i])-d;  //找到第一个大于它的d的下标,如果是最长上升子序列,这里变成lower_bound 
                d[j]=a[i]; 
            }
        }
        printf("%d\n",len);    
        return 0;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值