NCSTOJ:【动态规划】LIS最长上升子序列(1<=n<=10^5)

题目描述:

Description
给定一长度为n的数列,数列中的每个数都在1~100000之间

请在不改变原数列顺序的前提下,从中取出一定数量的整数(不需要连续),并使这些整数构成单调上升序列。

输出这类单调上升序列的最大长度。

Input
输入包括两行,第一行为n,代表数列的长度。(1 ≤ n ≤ 100000)

第二行为n个整数。

Output
输出这类单调上升序列的最大长度
Sample Input

5

3 1 5 4 6

Sample Output

3

More Info
对于样例的解释:356,156,146 均可组成长度为3的上升子序列

一、问题分析

1.找子问题:
“求序列的前n个元素的最长上升子序列”是一个子问题,但这个方法是行不通的,因为其不具有无后效性
设F(n)=x (前n个元素的最长上升子序列为x),但在前n个元素中,可能有多个上升子序列的值为x(结尾元素不一定是同一个,如2 3 7 6 5 6 2,当n=5时,237、236 、235)。有的序列的最后一个元素比第n+1个元素小,则能形成一个更长的子序列;有的序列的最后一个元素大于等于第n+1个元素,无法形成更长上升子序列。则当走到第n+1时,F(n+1)的值不仅和F(n)有关,还和前n个元素中的多个最长上升子序列的最后一个元素有关,和以前所了解的“只和上一个状态的值有关,与是如何到达上一个状态的方式无关”相矛盾。换句话说,F(n+1)的值应当只与F(n)的值有关。

换一个方向考虑,把子问题改成“求以a[k] (1<=k<=N)为终点的最长上升子序列的长度”,把所有的F(n) (以第n个元素结尾的最长上升子序列长度)都求出来后,只需要找出其中最大的就是答案。

2.确定状态:
如上文所说,"状态"就是以a[k] (1<=k<=N)为终点的最长上升子序列。

3.确定初始状态的值:
显然,F(1)=1;但是此处应当把所有的F初始值都设为1;不然轮到一个k,前1~k-1个元素没有比其小的,当在求后面的状态的值时,轮到k时其F(k)应当为1;

4.确定状态转移方程:
dp[i]=max(dp[i],dp[j]+1);

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;

int a[100005],dp[100005];

int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		dp[i]=1;
	}
	
	for(int i=2;i<=n;i++)
	{
		for(int j=1;j<i;j++)
		{
			if(a[j]<a[i])
			dp[i]=max(dp[i],dp[j]+1);
		}
	}
	
	cout<<*max_element(dp+1,dp+n+1);
}

二、改进

上述代码提交后你就会发现TLE了-_-||(因为时间复杂度为n^2) .
那么如何改呢。这里用了非DP方法,通过辅助数组s[],结合数列本身特性求解,s[]的长度即为答案。
操作:逐个处理a[]里面的数字(1)如果a[i]比辅助数组s[]的最后一个数更大,s[]长度加一,把a[i]添加到s[]结尾;(2)如果a[i]比s[]最后一个数小,就用lower_bound()二分查找s[]中第一个大于等于a[i]的位置,替换之。
初始化:s[1]=a[1];tot=1;

代码:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n,a[100010];
int s[100010],tot=0,tmp;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    s[++tot]=a[1];
    for(int i=2;i<=n;i++){
        if(a[i]>s[tot])s[++tot]=a[i];
        else{
            tmp=lower_bound(s+1,s+tot+1,a[i])-s;//STL的常用算法,二分查找s中第一个大于等于a[i]的数,用a[i]替换,复杂度为O(logn)(s[]必须有序)
            s[tmp]=a[i];
        }
    }
    printf("%d\n",tot);
    return 0;
}

解释:
为什么这样能行呢?对于操作(1),如果a[i]比辅助数组s[]的最后一个数更大,s[]长度加一,把a[i]添加到s[]结尾,很好理解。(2)"如果a[i]比s[]最后一个数小,就用lower_bound()二分查找s[]中第一个大于等于a[i]的位置,替换之。"首先,这个操作并不影响s[]的长度,即LIS的长度;其次,对于a[]中后面未处理的数字,可能很多都比s[]的最后一位小,但是有可能序列更长,这里的替换就给后面的更小的数字留下了机会,就不会错过形成更长LIS的可能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值