已知一个有N个数的数列 a0,a1,...,aN−1 ,求该数列的一个子序列 a′0,a′1,...,a′M−1 ,使 a′0≤a′1≤...≤a′M−1 ,且M尽量大。注意:子序列意思是 a′i,a′i+1 映射到 a 的编号不一定连续,但相对位置不变。
【输入】N,数列a .
【输出】M
一、 O(N2) :动态规划
在逐个加入元素的角度考虑,先假设已经加入了
a0,a1,...,ai−1
,形成了一些上升子序列。现在要加入
ai
。
那么,很明显可以想到一个算法:将
ai
连到一个结尾元素比它小的上升子序列后面,如果有多个,连到最长的一个后面。
用
f[i]
表示结尾元素编号是
i
的LIS的长度,那么有两种情况:
-
-
ai
比前面的一些元素(或全部元素)大,此时
ai
应该连接到以这些元素结尾的子序列中最长者的结尾,并且该子序列长度+1,即
f[i]=max(f[j]|j<i)+1
于是得状态转移方程:
所以它本质上就是动态规划。
f[0] = 1;
for (int i=1; i<N; i++)
{
f[i] = 1;
for (int j=0; j<i; j++)
if (a[j]<a[i])
f[i] = max(f[i], f[j]+1);
LIS_len = max(LIS_len, f[i]);
}
二、 O(NlogN) :动态规划+RMQ
上面的DP程序比较影响速度的就是这一段:
for (int j=0; j<i; j++)
if (a[j]<a[i])
f[i] = max(f[i], f[j]+1);
让我们回到前面的状态转移方程,可以看到,关键在于求f[0]~f[i-1]的最大值,也就是RMQ!
三、 O(NlogN) :单调性+二分查找
其实,f[i]除了表示结尾元素编号是
i
的LIS长度,还可以从另一种角度看。下面介绍的这种算法中,
我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的末尾元素比
ai
小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。
举个例子,还是在逐个加入元素的角度考虑,在加入
ai
时,之前有两个LIS,长度一致,但是结尾元素一个是10,一个是30。如果
ai≤10
,那两个都不能加;如果
ai>30
,两个LIS加哪个也无所谓。但如果
10<ai≤30
,那么很明显只能选结尾元素是10的LIS。要是再来一个长度相同的LIS,结尾元素是5,很明显选它是更优的。
上面这个例子说明在相同长度的前提下,LIS的结尾元素越小越好。因此,用
i
表示LIS的长度,
C++的 < algorithm > 库里就有两个二分搜索的函数,因此它的C++代码很短,很方便写。
lower_bound(first,last,val)
函数返回一个非递减序列[first, last)(包含 first 不包含 last)中的第一个大于等于值val的位置。first
与 last
均为指针类型,使用时类似 sort
,下同。
upper_bound(first,last,val)
函数返回一个非递减序列[first, last)中第一个大于val的位置。
这两个函数的时间复杂度都是
O(logN)
。
f[0]=-oo; f[1] = a[0];
for (int i=2; i<N; i++) f[i]=oo;
int ans=1;
for (int i=1; i<N; i++)
{
int* p = lower_bound(f,f+ans+2,a[i]);
if (a[i] < *p) *p = a[i];
if (f[ans+1] < oo) ans++;
}