最长严格上升子序列是一个十分常见的问题。这里我给出三种方法,一步步提升它的速度。
一、DP法
很容易想到它的转移方程f[i]=max(f[j])+1 (要求a[i]>a[j])这样一种O(n²)的方法,很容易理解。
二、DP+树状数组(或其他rmq算法)
树状数组用于优化求max(f[j])。但树状数组求区间最大值很有局限性,它要求这个最大值只能变大,一旦减小就无法更新。恰好f[j]也是不断增大的,于是就用上它了。
三、贪心+二分
新建一个数组b,用来放置一个数列。当某一个数比它末尾的元素大时,把它插在末尾,表示最长上升子序列的长度增加了1。要不就看看它能否替换这个序列中比它小的数。替换一个更小的数,那么这个数列就更加具有“潜力”,它允许部分原来比末尾数子大的数加入。这个替换的过程可以理解成,我们尝试把序列中的某个靠前的较大的数字换成了现在靠后的较小的数。比如在1,5,10,6,7,8中,在i=4时,1,5,10数列改成1,6,10可以理解成一个1,6,……(期待新数连接)的数列。在i=5时,7替换了10,数列成了1,6,7;相当于7连接了上去。这样后,在i=6,8才能自然地接上,数列变成1,5,6,7,8,增加了最长上升子序列的长度。
这样就做到了O(nlogn)的时间复杂度。
如果要求最长不下降子序列的长度,无须更改find_houji,只要在插入条件(35行)if(a[i]>b[len]) b[++len]=a[i]; 的>改成>=就可以了。当要求最长下降子序列时,find_houji要改成find_qianqu,其它的部分同求最长上升子序列的长度一样。
其中find_houji和find_qianqu均可以由系统函数代替:
C++的 <algorithm> 库里就有两个二分搜索的函数,使用它的C++代码很短,很方便写。lower_bound(first,last,val) :函数返回一个非递减序列[first, last)(包含 first 不包含 last)中的第一个大于等于值val的位置。first 与 last 均为指针类型,使用时类似 sort,下同。
upper_bound(first,last,val) :函数返回一个非递减序列[first, last)中第一个大于val的位置。
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=1010;
int n;
int len;
int a[maxn],b[maxn];
int find_houji(int k)//寻找可相等的后继 b[ans]>=k
{
int l=1,r=len,ans=1;
while(l<=r)
{
int mid=l+r>>1;
if(b[mid]>=k)
{
ans=mid;
r=mid-1;
}
else
{
l=mid+1;
}
}
return ans;
}
void get_up()
{
len=1;b[1]=a[1];
for(int i=2;i<=n;i++)
{
if(a[i]>b[len]) b[++len]=a[i];//加入数列
else
{
int p=find_houji(a[i]);//寻找数列中a[i]的后继
b[p]=a[i];//替换数列中的数
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i])
get_up();
printf("%d\n",len);
return 0;
}