动态规划:最长上升子序列(二分算法 nlogn)「建议收藏」

动态规划:最长上升子序列(二分算法 nlogn)「建议收藏」解题心得:1、在数据量比较大的时候n^2会明显超时,所以可以使用nlogn的算法,此算法少了双重循环,用的lower_bound(二分法)。2、lis中的数字并没有意义,仅仅是找到最小点lis[0]和最大点lis[len],其中,在大于lis[len]时len++,在小于lis[len]时可以将arr[i]在lis中的数进行替换掉。所以此算法主要是在不停的找最合适的起点和最合适的终点。

大家好,又见面了,我是你们的朋友全栈君。

早些年间写的博客,当时对算法还不是很熟悉,所以写的很简略,也没说清楚,有不少留言提问,现在重新修改这个博客。

增添:(增添的内容看不懂的可以先看算法,然后在返回来看增添的内容)
首先明白两个二分函数:

  • l o w e r lower lower_ b o u n d ( s t a r t P o s , e n d P o s , v a l u e ) bound(startPos, endPos, value) bound(startPos,endPos,value):在区间 [ s t a r t P o s , e n d P o s ) [startPos, endPos) [startPos,endPos)内找到第一个大于等于 v a l u e value value的地址,如果找不到则返回 e n d P o s endPos endPos,注意的是区间左闭右开。
  • u p p e r upper upper_ b o u n d ( s t a r t P o s , e n d P o s , v a l u e ) bound(startPos, endPos, value) bound(startPos,endPos,value):在区间 [ s t a r t P o s , e n d P o s ) [startPos, endPos) [startPos,endPos)内找到第一个大于 v a l u e value value的地址,如果找不到则返回 e n d P o s endPos endPos,注意的是区间左闭右开。

而这个 n ∗ l o g n n*logn nlogn复杂度算法的思想是什么呢?就是维护一个递增数列,让数列整体的值尽量的小,这样当出现新的一个数的时候,能加进数列并且不改变数列递增性质的可能才更大。而这个数列本身是没有意义的,数列里面的数并不是你找到的上升子序列。

假设当一个新的数出现的时候有两种情况,第一种是这个数比递增数列中的最后一个数更大,那直接加入,数列的长度加一。第二种是这个数加入数列最后一位后改变了数列的递增性质,这个时候拿这个数替换数列中存在的第一个比它大的数(一定能找到),替换之后让数列总体尽可能的小并且没有改变数列的长度,所以符合无后效性,按照这样得到的答案是合法的。

所以使用 l o w e r lower lower_ b o u n d bound bound还是使用 u p p e r upper upper_ b o u n d bound bound都是一样的(可能写法不一样),是得到递增子序列还是不递减子序列是在最后加入的数是大于数列最后一个数还是大于等于数列最后一个数。

这个时候再多想一想,我们在遇到 n u m [ i ] num[i] num[i]这个数时,将这个数在数列中二分查找出来的那个位置有什么意义,那么记找到的这个位置为 p o s pos pos p o s pos pos代表的就是以 n u m [ i ] num[i] num[i]这个数为结尾的单调数列的最长长度,这个时候才需要区分到底是递增数列还是不递减数列来选择使用 l o w e r _ b o u n d lower\_bound lower_bound还是使用 u p p e r _ b o u n d upper\_bound upper_bound得到最长长度。
增添结束


解题心得:
1、在数据量比较大的时候 n 2 n^2 n2会明显超时,所以可以使用 n ∗ l o g n n*logn nlogn 的算法,此算法少了双重循环,用的 u p p e r b o u n d upper_bound upperbound(二分法)。
2、 l i s lis lis中的数字并没有意义,仅仅是找到最小点 l i s [ 0 ] lis[0] lis[0]和最大点 l i s [ l e n ] lis[len] lis[len],其中,在大于 l i s [ l e n ] lis[len] lis[len] l e n + + len++ len++,在小于 l i s [ l e n ] lis[len] lis[len]时可以将 a r r [ i ] arr[i] arr[i] l i s lis lis中的数进行替换掉。所以此算法主要是在不停的找最合适的起点和最合适的终点。


引用:
假设存在一个序列d[1…9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。n
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1…2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1…2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1…3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1…3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1…5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。
!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!!


题目:

Description

设有一个序列 ( a 1 , a 2 , . . a n ) (a_{1},a_{2},..a_{n}) a1,a2,..an,如果序列 ( a i 1 , a i 2 , . . . , a i 3 ) (a_{i1}, a_{i2} , …, a_{i3}) (ai1,ai2,...,ai3), 满足 1 &lt; = i 1 &lt; i 2 &lt; . . . &lt; i k &lt; = n 1&lt; = i_{1} &lt; i_{2}&lt; …&lt; i_{k}&lt;=n 1<=i1<i2<...<ik<=n a i 1 &lt; a i 2 &lt; . . . &lt; a i k a_{i1} &lt; a_{i2}&lt; .. .&lt; a_{ik} ai1<ai2<...<aik,则称 ( a i 1 , a i 2 , . . . , a k ) 为 ( a 1 , a 2 , … … a n ) (a_{i1}, a_{i2}, …, a_{k})为(a_{1},a_{2},……a_{n}) (ai1,ai2,...,ak)(a1,a2,an)的一上升子序列。 例如序列1,6,2,5,4,7的一个最长上升子序列是1,2,5,7(还有其他的,这里略去),长度是4.给一个序列,求它的最长上升子序列的长度。

Input

题目有多组测试数据,每组第一行为一个整数n,代表序列的长度,第二行为序列的n个数。(1<=n<=100000)。//数据量比较大经典算法不合适。

Output

每组输出占一行,包含序列的最长上升子序列的长度。

Sample Input

7
1 7 3 5 9 4 8

Sample Output

4

#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
int main()
{ 
   
    int n,num[100100],lis[100100],len;
    while(scanf("%d",&n)!=EOF)
    { 
   
        len = 0;
        memset(lis,0,sizeof(lis));
	    for(int i=0;i<n;i++)//如果i是从1开始,在lower_bound中的到的位置会返回到0,这样就不可以把lis[1]的位置替换掉,从而WA。
        { 
   
            scanf("%d",&num[i]);
        }
        lis[0] = num[0];
        for(int i=1;i<n;i++)
        { 
   
            if(num[i] > lis[len])//如果num比lis[len]选择的终点大,则可以放入lis,即新的终点。
                lis[++len] = num[i];
            else
            { 
   
                int pos = upper_bound(lis,lis+len,num[i]) - lis;//注意upper_bound 的用法,upper_bound返回的是第一个比它大的数的一个地址
                lis[pos] = num[i];//!!!
            }
        }
        printf("%d\n",len+1);//len是从0开始的,所以要加上1。
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/132005.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号