问题描述:
所谓子序列,就是在原序列里删掉若干个元素后剩下的序列,以字符串"abcdefg"为例子,去掉bde得到子序列"acfg"
现在的问题是,给你一个数字序列,你要求出它最长的单调递增子序列。
输入:
多组测试数据,每组测试数据第一行是n(1<=n<=10000),下一行是n个比1e9小的非负整数
输出:
对于每组测试数据输出一行,每行内容是最长的单调递增子序列的长度
样例输入:
5
1 2 4 8 16
5
1 10 4 9 7
9
0 0 0 1 1 1 5 5 5
样例输出:
5
3
3
[分析]
最开始拿到这个题目,很容易会想到是打导弹(最长不上升子序列)那个题目的原型的变种,于是就有一个很容易会想到的思路:动态规划。设Length[i]表示序列Seq中以Seq[i]为最末元素时的递增序列的长度。则有如下状态转移方程:Length[i]=max{Length[j]|0<=j<=i-1 and Seq[j]<Seq[i]} (1<=i<n) 边界条件:Length[i]=1 (0<=i<n)。
所以就有如下实现代码:
vector<int> Length(n, 1);
for (size_t i=1; i<Seq.size(); i++)
{
Max=0;
for (size_t j=0; j<i; j++)
if (Seq[j]<Seq[i] && Length[j]>Max) Max=Length[j];
Length[i]=Length[i]+Max;
}
Length中的最大值即为序列Seq中的最长递增子序列的长度。显然,这是个T(n)=O(n^2)的算法。当时我就是以此思路写的代码提交,结果最后一个数据超时,显然上述算法的效率不符合要求(小鹏后来用数组加scanf用这方法过了,证明效率也不是很低,而我当时用的cin和容器...)。
在上面的算法中,计算每一个Length[i]时都要找出最大的Length[j](j<i),而Length显然是无序的,只能顺序查找。如果能想办法把这个顺序查找变为二分查找,那么时间复杂度将由O(n^2)降为O(nlogn)。想了很久,没有思路,去baidu了一下,找到一种思路:建立个数组Last来储存递增子序列的末尾元素,使得Last[Length[j]]=Seq[j-1],显然这样储存会使Last中的元素呈递增排列。于是上面方法中用线性查找寻找Length中最大元素并更新Length的操作,就可以变成在数组Last中用二分查找找到满足Last[k]<Seq[i]的最大的k,并将Last[k+1]置为Seq[i]。所以状态转移方程就为:k=max{k|Last[k]<Seq[i]} (1<=i<n) 边界条件:Last[0]=INT_MIN,Last[1]=Seq[1]。那么最直接的实现方法就是下面的方法二。
因为之前还参考了小鹏的思路写了个代码(即下面的方法一),在理解了方法二这种算法后我习惯性的将两种算法作了对比,结果发现两者惊人的相似!方法一中set<T>类的成员函数insert(T val)的功能和插入排序类似,插入排序插入元素的过程正好是将待插入数据插入第一个大于待插入数据的数组元素的前面,而在插入过后如果不是插在末尾就会立刻删除其后一个元素(即待插入元素如果不是放在末尾就会替换掉老数据),这就和第二种方法在数组Last中用二分查找找到满足Last[k]<Seq[i]的最大的k并将Last[k+1]置为Seq[i]这一操作有异曲同工之妙了!
[Code]
方法1:
#include <iostream>
#include <set>
using namespace std;
int main()
{
size_t n;
while (cin>>n)
{
set<long> Last;
for (size_t i=0; i<n; i++)
{
long Temp;
set<long>::iterator Index;
cin>>Temp;
Last.insert(Temp);
Index=Last.find(Temp);
if (++Index!=Last.end()) Last.erase(Index);
}
cout<<Last.size()<<endl;
}
return 0;
}
方法2:
#include <iostream>
#include <vector>
using namespace std;
size_t LISLength(vector<long> &Seq)
{
vector<long> Last(Seq.size()+1); //以Last[i]为末尾元素的递增子序列长度为i
size_t Length=1;
Last[0]=INT_MIN;
Last[1]=Seq[0];
for (size_t i=1; i<Seq.size(); i++)
{
size_t Front=0, Rear=Length, Middle;
while (Front<=Rear) //二分查找末尾元素小于Seq[i]的长度最大的递增子序列
{
Middle=(Front+Rear)/2;
if (Last[Middle]<Seq[i]) Front=Middle+1;
else Rear=Middle-1;
}
Last[Front]=Seq[i];
if (Front>Length) Length=Front;
}
return Length;
}
int main()
{
size_t n;
while (cin>>n)
{
vector<long> Seq;
int Temp=0;
for (size_t i=0; i<n; i++)
{
cin>>Temp;
Seq.push_back(Temp);
}
cout<<LISLength(Seq)<<endl;
}
return 0;
}
以下代码可以很容易的修改成计算最长下降、不上升、不下降子序列的长度,仅仅是修改Last的初始化以及二分搜索时改变头尾指针的条件。这个算法与O(n^2)不同之处在于该算法巧妙的转换了一下状态,使之可以用二分搜索来提高效率。
int LIS(const int *pcnSeq, int nLen)
{
int l=0, *pnLast=new int[nLen+1];
pnLast[0]=INT_MIN;
for (int i=0; i<nLen; i++)
{
int p=0, r=l;
while (p<=r)
{
int m=(p+r)/2;
if (pnLast[m]<pcnSeq[i]) p=m+1;
else r=m-1;
}
pnLast[p]=pcnSeq[i];
if (p>l) l=p;
}
delete[] pnLast;
return l;
}
先回顾经典的O(n^2)的动态规划算法,设A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F [t] = 0(t = 1, 2, ..., len(A))。则有动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, ..., t - 1, 且A[j] < A[t])。
现在,我们仔细考虑计算F[t]时的情况。假设有两个元素A[x]和A[y],满足
(1)x < y < t (2)A[x] < A[y] < A[t] (3)F[x] = F[y]
此时,选择F[x]和选择F[y]都可以得到同样的F[t]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?
很明显,选择A[x]比选择A[y]要好。因为由于条件(2),在A[x+1] ... A[t-1]这一段中,如果存在A[z],A[x] < A[z] < a[y],则与选择A[y]相比,将会得到更长的上升子序列。
再根据条件(3),我们会得到一个启示:根据F[]的值进行分类。对于F[]的每一个取值k,我们只需要保留满足F[t] = k的所有A[t]中的最小值。设D[k]记录这个值,即D[k] = min{A[t]} (F[t] = k)。
注意到D[]的两个特点:
(1) D[k]的值是在整个计算过程中是单调不上升的。
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断A[t]与D[len]。若A [t] > D[len],则将A[t]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = A [t];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < A[t]。令k = j + 1,则有D[j] < A [t] <= D[k],将A[t]接在D[j]后将得到一个更长的上升子序列,同时更新D[k] = A[t]。最后,len即为所要求的最长上升子序列的长度。
在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!
这个算法还可以扩展到整个最长子序列系列问题,整个算法的难点在于二分查找的设计,需要非常小心注意。
可以参考一下ZOJ-1986 这题目。
http://acm.zju.edu.cn/show_problem.php?pid=1986
solutiion for zoj 1986:
刘汝佳的做法