LIS及其扩展
1计算长度
1.1朴素DP
我们设状态\(f_i\)为以i结束的最长上升子序列的最长长度。则有则我们从i前面找到一个元素,满足\(a_j<a_i\),枚举所有满足条件的j,则i的f值就是这所有的j对应的f值加1。代码:
for(int i=1;i<=n;++i)
{
for(int j=1;j<i;++j)
if(a[j]<a[i])
f[i]=max(f[i],f[j]+1);
}
1 .2优化dp
我们发现我们浪费了一些时间在找满足\(a_j<a_i\)的f的最大值上,能否优化这个过程呢?当然可以,我们只需要一颗线段树。这颗线段树满足单点修改,区间查询最大值,所以不用打懒标记。
这里需要注意的是,线段树里的每个位置是\(a_i\)这个值,而不是下标i,每次找到以i结尾的最大值时单点查询\(a_i\),把这个位置的值改为\(f_i\),到了\(a_j\)查找最大值时查找的区间就是0至\(a_{j-1}\)这样就保证了我们解得合法性。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 7001000
#define M 100100
using namespace std;
inline int Max(int a,int b){
return a>b?a:b;
}
struct Xtree{// point_change square_ask->no_lazy
int p[N];
inline Xtree(){
memset(p,0,sizeof(p));
}
inline void pushup(int k){
p[k]=Max(p[k*2],p[k*2+1]);
}
inline void change(int k,int l,int r,int w,int x){
if(l==r&&l==w){
p[k]=x;
return;
}
int mid=l+r>>1;
if(w<=mid) change(k*2,l,mid,w,x);
else change(k*2+1,mid+1,r,w,x);
pushup(k);
}
inline int ask_max(int k,int l,int r,int z,int y){
if(l==z&&r==y) return p[k];
// printf("%d %d %d %d\n",l,r,z,y);
int mid=l+r>>1;
// printf("%d %d %d\n",l,r,mid);cin.get();
if(mid<z) return ask_max(k*2+1,mid+1,r,z,y);
else if(y<=mid) return ask_max(k*2,l,mid,z,y);
else return Max(ask_max(k*2,l,mid,z,mid),ask_max(k*2+1,mid+1,r,mid+1,y));
}
};
int n,a[M],maxx=-1;;
Xtree xt;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
xt.change(1,0,M,a[1],1);
// printf("enter\n");
for(int i=2;i<=n;i++){
int w=xt.ask_max(1,0,M,0,a[i]-1);
// printf("%d ",w);
maxx=Max(maxx,w+1);
xt.change(1,0,M,a[i],w+1);
}
printf("%d",maxx);
return 0;
}
1.3第二种优化
这一次我们的优化要重新设计状态。
思考这样一个问题,我们能否使得我们的合法序列为有序的,从而可以二分取寻找最优值?
我们这样来设计状态:
设\(f_i\)表示以长度为i的最长上升子序列的结尾的数值(不是下标)的最小值。
首先关注一下这个f数组的有序性。
我们发现这个f数组一定是一个单调递增的序列,否则,如果存在一个\(f_i\)满足\(i<j\)且\(f_i>=f_j\),以\(f_j\)结尾的长度为j的最长上升子序列,设它的第i项为k,则以k结尾的长度为i的最长上升子序列一定存在,原因就是j比i要长,并且k比\(f_i\)更优,故一定是一个单调递增序列。
那么我们怎么来利用它的单调性呢?
我们先考虑怎么用先有的序列的每一个元素去维护f数组。
我们设a数组为我们要求的最长上升子序列的那个题目给出的数组。
设此时此刻该用\(a_i\)去更新维护我们的f数组,设当前f数组的长度为len,由f数组的定义可以知道,那么当前a数组的最长上升子序列为len。
容易想到的是,如果\(f_len\)要小于\(a_i\)的话,那么我们可以另\(f_{++len}=a_i\),更新当前最长上升子序列的最优值。
接下来最重要的问题,也是这个算法的主体,就是\(a_i\)可能可以更新len以前f数组的值,容易想到,如果满足\(f_len\)要小于\(a_i\),那\(a_i\)就没法去更新其余的f值,如果不是这种情况呢?
我们思考一下,可以得出以下结论:
如果\(f_q\)比\(a_i\)要小,那么\(a_i\)就可以接在它后面,去更新长度为q+1的最长上升子序列的末尾值。
但是如果\(f_{q+1}\)要比\(a_i\) 要小的话,显然,虽然\(a_i\)满足条件,但是却不能够更新\(f_{q+1}\)。
那什么时候\(a_i\)才能更新呢?
当且仅当\(a_i\)比\(f_q\)要大并且\(a_i\)要比\(f_{q+1}\)要小!
又因为f数组是一个单调上升的数组。
所以我们可以在f数组里二分。
这里二分推荐使用lower_bound和upper_bound,这两个函数不仅在STL里有,其余时候也可以用。只是我习惯打很多很多的头文件,因此不知道这两个函数在哪个头文件里。。。算了,无伤大雅。
顺便说一句。前者返回的是第一个大于等于的值,后者返回的是第一个大于的值。
具体用法看代码,upper_bound类似
其实也可以自己打一个二分查找,二分不算太难。
代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;
int a[N],f[N],n;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int len=1;
f[len]=a[1];
for(int i=2;i<=n;i++){
if(a[i]>f[len]) f[++len]=a[i];
else{
int w=lower_bound(f+1,f+len+1,a[i])-f;
f[w]=a[i];
}
}
printf("%d",len);
}
2扩展
2.1求最长不下降子序列。
其实求最长不下降子序列的方法类似,读者不妨自己去推一下。这里只放代码
线段树:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 400100
#define M 100100
using namespace std;
inline int Max(int a,int b){
return a>b?a:b;
}
struct Xtree{//point_change square_ask no_lazy
int p[N<<3];
inline void pushup(int k){
p[k]=Max(p[k*2],p[k*2+1]);
}
inline void change(int k,int l,int r,int w,int x){
if(l==r&&r==w){
p[k]=x;
return;
}
int mid=l+r>>1;
// printf("%d %d %d %d\n",l,r,mid,w);cin.get();
if(w<=mid) change(k*2,l,mid,w,x);
else change(k*2+1,mid+1,r,w,x);
pushup(k);
}
inline int ask_max(int k,int l,int r,int z,int y){
if(l==z&&r==y) return p[k];
int mid=l+r>>1;
if(y<=mid) return ask_max(k*2,l,mid,z,y);
else if(mid<z) return ask_max(k*2+1,mid+1,r,z,y);
else return Max(ask_max(k*2,l,mid,z,mid),ask_max(k*2+1,mid+1,r,mid+1,y));
}
};
Xtree xt;
int n,a[N],b[N],maxx;
int main(){
// freopen("dp.out","r",stdin);
// freopen("1.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
xt.change(1,0,M<<1,a[1],1);
for(int i=2;i<=n;i++){
int w=xt.ask_max(1,0,M<<1,0,a[i]);
maxx=Max(maxx,w+1);
xt.change(1,0,M<<1,a[i],w+1);
}
printf("%d\n",n-maxx);
}
二分:
f[1]=a[1];int len=1;
for(int i=2;i<=tail;i++)
{
if(a[i]<=f[len]) f[++len]=a[i];
else
{
int l=1,r=len;
while(l<r)
{
int mid=l+r>>1;
if(f[mid]<a[i]) r=mid;
else l=mid+1;
}
f[l]=a[i];
}
}
这里手写了一个二分,主要是因为做导弹拦截的时候还不会lower_bound和upper_bound,那时候的码风也和现在不同。
2.2求最长公共子序列。
接下来的主要介绍将最长公共子序列转化为最长上升子序列来求解。
我们设第一个序列为\(a_1,a_2,...a_n\),第二个序列为\(b_1,b_2,...b_m\)
接下来的操作是,对a中的元素从小到大排序,变成\(a_i,a_j,...a_k\),a数组排序前后的两个不同的数组之间设置一个映射关系,即有 \(a_1\rightarrow a_i,a_2\rightarrow a_j,...a_n\rightarrow a_k\),b中属于a中的元素,则也做此类映射,同时b中不属于a的元素全部去掉,程序中实现时只需要标记一下。
这里需要再加上一个操作,映射后,如果a中有q个x元素,b中有p个,如果p比q小,那么要把着p个元素减到q个。不然结果会出错。
这样在映射后,a数组变成了一个不下降的序列,只需要对b数组跑一个最长不下降子序列即可,因为在映射后,b中任何一个不下降子序列都是与a的一个公共子序列。
洛谷上的题目:https://www.luogu.com.cn/problem/P1439
这个题目有一定的特殊性,因为都是1到n的一个全排列,所以去重操作就不用了。
\(O(nm)\) 做法
#include<iostream>
using namespace std;
int dp[1001][1001],a1[2001],a2[2001],n,m;
int main()
{
//dp[i][j]表示两个串从头开始,直到第一个串的第i位
//和第二个串的第j位最多有多少个公共子元素
cin>>n>>m;
for(int i=1;i<=n;i++)scanf("%d",&a1[i]);
for(int i=1;i<=m;i++)scanf("%d",&a2[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a1[i]==a2[j])
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
//因为更新,所以++;
}
cout<<dp[n][m];
}
\(O(nlogn)\)做法
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;
int n;
int a[N],b[N];
int m[N],f[N];
inline int Min(int a,int b)
{
return a>b?b:a;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
m[a[i]]=i;
}
for(int i=1;i<=n;i++)
{
scanf("%d",&b[i]);
b[i]=m[b[i]];
f[i]=0x7ffffff;
}
f[1]=b[1];int len=1;
for(int i=2;i<=n;i++)
{
int l=1,r=len;
if(b[i]>f[len]) f[++len]=b[i];
else
{
while(l<r)
{
int mid=l+(r-l>>1);
if(f[mid]>b[i]) r=mid;
else l=mid+1;
}
f[l]=Min(f[l],b[i]);
}
}
printf("%d",len);
return 0;
}
引用