对于求最长上升子序列问题,朴素算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),数据范围在
1
0
5
10^5
105以上时会超时。
以P1020 [NOIP1999 普及组] 导弹拦截为例,对于第一问求最长不下降子序列及第二问求最长上升子序列,可以采用二分的方法将时间复杂度降为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),具体操作如下
-
对于dp数组,我们在 d p [ i ] dp[i] dp[i]中存放长度为 i i i的上升子序列的尾元素。例如 d p [ 3 ] dp[3] dp[3]中存放长度为 3 3 3的上升子序列的末尾元素。
-
在扫描数列过程中记录当前最长上升子序列长度为 m a x l e n maxlen maxlen
-
将 d p [ 1 ] dp[1] dp[1]初始化为数列第一个元素( d p [ 1 ] = a [ 1 ] dp[1]=a[1] dp[1]=a[1]),而后对数列进行扫描:若当前元素大于当前最长上升子序列的尾元素,即
a [ i ] > d p [ m a x l e n ] a[i]>dp[maxlen] a[i]>dp[maxlen],那么当前数字可以加入最长上升子序列,则 m a x l e n + + maxlen++ maxlen++,并将该长度的末尾值设置为 a [ i ] a[i] a[i]。反之,寻找该元素是否可以替换长度为1~maxlen的上升子序列的末尾值。因为对于相同长度的上升子序列,其尾元素越小,在向后扫描的过程中越可能找到加入该子序列的元素。 由于对于长度递增的序列,其尾元素值也必定递增,所以对于这一过程,我们可以采用二分来实现。
具体实现代码如下
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll mod=1e6+7;
const int INF=0x7fffff;
int a[100005]; //原数组
int dp1[100005]; //存放非上升子序列长度为i的末尾元素
int dp2[100005]; //存放上升子序列长度为i的末尾元素
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n=0;
while(scanf("%d",&a[++n])!=EOF){}
n--;
/*
以下为第一问实现部分
*/
dp1[1]=a[1]; //将长度为1的序列尾元素初始化为1
int maxlen=1; //初始长度为1
for(int i=2;i<=n;i++)
{
//该元素大于当前最大长度序列的尾元素,则直接加入
if(a[i]<=dp1[maxlen])
dp1[++maxlen]=a[i];
//该元素小于等于当前最大长度序列的尾元素,则查找是否能更新其余长度的尾元素
else
{
int l=1,r=maxlen,mid;
while(l<r)
{
mid=(l+r)/2;
if(a[i]<=dp1[mid])
l=mid+1;
else
r=mid;
}
dp1[l]=max(dp1[l],a[i]);
}
}
cout<<maxlen<<endl;
/*
以下为第二问实现部分
*/
fill(dp2+1,dp2+n+1,INF); //由于寻找LIS是将尾元素值尽可能变小,所以要将dp数组初始化为INF
//以下实现过程同第一问
dp2[1]=a[1];
maxlen=1;
for(int i=2;i<=n;i++)
{
int l=1,r=maxlen,mid;
if(a[i]>dp2[maxlen])
{
dp2[++maxlen]=a[i];
}
else
{
while(l<r)
{
mid=(l+r)/2;
if(a[i]<=dp2[mid])
r=mid;
else
l=mid+1;
}
dp2[l]=min(dp2[l],a[i]);
}
}
cout<<maxlen<<endl;
return 0;
}