最长上升子序列 ( L o n g e s t I n c r e a s i n g S u b s e q u e n c e ) (Longest \;Increasing\;Subsequence) (LongestIncreasingSubsequence),简称 L I S LIS LIS,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。
我们知道,对于最长上升子序列,假设我们用
f
i
f_i
fi表示出当前位置为
i
i
i的最长上升子序列的长度,可以写出一个
D
P
DP
DP方程
f
i
=
max
j
<
i
(
f
j
)
+
1
(
a
j
<
a
i
)
f_i=\max_{jfi=j<imax(fj)+1(aj<ai)
因此,对于每一个
i
i
i,我们只需要枚举
i
i
i之前的
j
j
j的,依次转移比大小即可。复杂度显然是
O
(
n
2
)
O(n^2)
O(n2)的。
其实
L
I
S
LIS
LIS是一个典型的二维偏序问题,二维偏序的核心思路就是通过树状数组进行优化。
我们再来回顾
O
(
n
2
)
D
P
O(n^2)DP
O(n2)DP的状态转移方程:
f
[
i
]
=
max
(
f
[
j
]
)
+
1
(
1
≤
j
<
i
,
A
[
j
]
≤
A
[
i
]
)
f[ i ]=\max (f[ j ])+1\;(1\le j\lt i,A[j]\le A[i])
f[i]=max(f[j])+1(1≤j<i,A[j]≤A[i])
我们在递推
f
f
f数组的时候,每次都要把
f
f
f数组扫一遍求
f
[
j
]
f[j]
f[j]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。用树状数组来维护
f
f
f数组(据说分块也是可以的,但是分块是
O
(
n
n
)
O(n\sqrt n)
O(nn)的时间复杂度,不如树状数组跑得快),首先把
A
A
A数组从小到大排序,同时把
A
[
i
]
A[ i ]
A[i]在排序之前的序号记录下来。然后从小到大枚举
A
[
i
]
A[ i ]
A[i],每次用编号小于等于
A
[
i
]
A[ i ]
A[i]编号的元素的
L
I
S
LIS
LIS长度
+
1
+1
+1来更新答案,同时把编号大于等于
A
[
i
]
A[ i ]
A[i]编号元素的
L
I
S
LIS
LIS长度
+
1
+1
+1。因为
A
A
A数组已经是有序的,所以可以直接更新。有点绕,具体看代码。
#include
#define in read()
#define MAXN 100050
#define re register
#define INF (1<<30)
#define lb(x) x&(-x)
using namespace std;
struct node{
int id,val;
bool operator < (const node &rhs)const{
return val==rhs.val?id<rhs.id:val<rhs.val;
}
}a[MAXN];
int n,t[MAXN],ans=0;
inline int read(){
int x=0,f=1;char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){x=(x<<3)+(x<<1)+c-'0';c=getchar();}
return x*f;
}
inline void update(int x,int v){
for(;x<=n;x+=lb(x)){t[x]=max(t[x],v);}
}
inline int query(int x){
int res=-INF;
for(;x;x-=lb(x)){res=max(res,t[x]);}
return res;
}
int main(){
n=in;
for(re int i=1;i<=n;i++)a[i].val=in,a[i].id=i;
sort(a+1,a+n+1);
for(re int i=1;i<=n;i++){
int maxx=query(a[i].id);
update(a[i].id,++maxx);
ans=max(ans,maxx);
}
cout<<ans<<'\n';
return 0;
}
让我们理性思考一下这个算法的正确性,因为我们按照数的大小进行的排序,那么后被扫描到的数就一定比先扫描到的数更大,满足了一个条件。此时我们只需要再满足大的数的下标比小的数的下标大即可,而树状数组巧妙解决了这个问题。