浅谈最长上升子序列的二分优化
浅谈最长上升子序列的二分优化
最长上升子序列=LIS。
这人在学习 LIS 后一年多才会 LIS 的二分优化,是不是废了。。
省流:贪心+二分,\(n^2\rightarrow n\log n\)。
大概题意
给定一个长度为 \(n\) 的序列 \(a\),求 \(a\) 的最长上升子序列长度。
定义一个长度为 \(k\) 的序列 \(b\) 是最长上升子序列,当且仅当 \(b_1<b_2<b_3\cdots b_{k-1}<b_k\)。
普通动态规划方法
记 \(f_i\) 表示以 \(a_i\) 为结尾的最长 LIS 长度。则转移方程为 \(f_i=\max_{j=1}^{i-1}\left \{f_j+1(a_j<a_i)\right\}\)。
意为在下标为 \([1,i-1]\) 中枚举每一个满足 \(a_j<a_i\) 的数,尝试在 \(a_j\) 后接入 \(a_i\) 所带来的 LIS 长度。
时间复杂度为 \(O(n^2)\)。有时候并不能过。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005;
int n,ans;
int a[MAXN],f[MAXN];
inline int Read()
{
int num=0,r=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-')
r=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
num=num*10+ch-'0';
ch=getchar();
}
return num*r;
}
int main(){
n=Read();
for(int i=1;i<=n;i++)
{
a[i]=Read();
f[i]=1;
}
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(a[i]>a[j])
f[i]=max(f[i],f[j]+1);
for(int i=1;i<=n;i++)
ans=max(ans,f[i]);
printf("%d\n",ans);
return 0;
}
二分+贪心优化
考虑这么一件事情:
如果有两个长度为 \(k\) 的 LIS,一个结尾为 \(c\),一个结尾为 \(d\),且 \(c<d\)。那么你会如何更新当前的 \(f_i\)?
显然因为 \(c<d\),那么可以用 \(d\) 来更新的 LIS 一定可以被 \(c\) 更新。所以考虑贪心。在 LIS 长度相同的情况下,只保留末尾数字最小的。
所以我们可以再开一个数组 \(g\),\(g_i\) 表示 LIS 长度为 \(i\) 的最小的结尾数字。
那么我们更新 \(f_i\) 的时候就可以二分找到 \(g\) 的第一个大等于 \(a_i\) 的下标 \(pos\)。
因为 \(g_{pos}\ge a_i\),所以只能用 \(g_{pos-1}\) 来更新 \(f_i\)。那么 \(f_i=pos-1+1=pos\)。
为什么要加一?因为普通的 LIS 转移就是从前边加上一个长度。而前边的长度为 \(pos-1\)。所以答案为 \(pos\)。
再来看看如何更新 \(g\) 数组。
因为 \(g_{pos}\ge a_i\),且 \(f_i=pos\)。所以把 \(g_{pos}\) 更新为 \(a_i\) 一定是不劣的。所以可以直接将 \(g_{pos}\) 更新为 \(a_i\)。
最后再更新一波 \(g\) 的长度即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,a[N],f[N],g[N],glen,maxn;
int main(){
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=1;i<=n;++i)
{
int pos=lower_bound(g+1,g+glen+1,a[i])-g;//找到第一个大于等于a_i的位置
f[i]=pos;//上述更新
g[pos]=a[i];//上述更新
glen=max(glen,pos);//因为有可能 a_i 结尾的LIS长度为 glen+1,所以需更新
}
for(int i=1;i<=n;++i)
maxn=max(maxn,f[i]);
cout<<maxn<<'\n';
return 0;
}
从此,时间复杂度就从 \(O(n^2)\rightarrow O(n\log n)\)。

浙公网安备 33010602011771号