浅谈最长上升子序列(LIS)
一、瞎扯的内容
给一个长度为n的序列,求它的最长上升子序列(LIS)
简单的dp
n=read(); for(i=1;i<=n;i++) a[i]=read(); for(i=1;i<=n;i++) for(j=1;j<i;j++) if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1); printf("%d",dp[n]);
然后发现
看来需要一个nlogn求LIS的算法
二、不瞎扯的内容
上一个算法慢在哪里呢?内层的循环
如果把它变成二分查找不就是nlogn的算法了吗
为此需要进行一下改动
dp数组改为存储长度为i的上升子序列中最小的末尾数字
这样每当外层循环到了一个新的数字
若
a[i]>dp[len]//新的数字比当前LIS的末尾数字大
则
dp[++len]=a[i]//愉快地把这个数字接到后面
如果它小于等于dp[len]?
举个例子
序列2 3 1
i=1:找到2,当前LIS={2},没毛病;
i=2:找到3,发现它比LIS的最后一个数字大,把它接到后面,当前LIS={2,3};
i=3:找到1,发现它比3小,那么在当前的LIS中找到一个最小的比1大的数,也就是2,并把2替换成1,当前LIS={1,3}
Q1:为什么可以这么做呢?
我们现在把2替换成了1,如果接下来还能把3替换,我们就可以得到一个更优的LIS
为什么更优?首先LIS是保证尽可能长的,在此基础上,末尾数字越小越优
Q2:如何在当前的LIS中找到一个最小的比a[i]大的数?
根据“长度一定时,末尾数字越小结果越优”原则,dp数组一定是递增的
这个递增就不做证明了,应该比较好想
所以就可以用二分查找了
问题解决
for(i=1;i<=n;i++) dp[i]=0x7fffffff; dp[1]=a[1]; int len=1,l,r,mid; for(i=2;i<=n;i++) { if(a[i]>dp[len]) dp[++len]=a[i]; else { l=0;r=len; while(l<=r) { mid=(l+r)>>1; if(dp[mid]>a[i]) r=mid-1; else l=mid+1; } dp[l]=min(dp[l],a[i]); } }
三、来道题
题目简述:给出两个1~n的序列,求他们的最长公共子序列(n≤100000)
这跟LIS有什么关系呢?
还真有关系
不妨改为在第二个序列中匹配第一个序列,最多匹配多少
由于两个序列都是1~n的,可将第一个序列重新定义一下
比如 3 2 1 4 5
3----1;2----2;1----3;4----4;5----5(对应到它是第几个)
那么第二个序列 1 2 3 4 5就会变成3 2 1 4 5
这时再求最长公共子序列的答案是一样的
然而可以发现第一个序列已经变成了1~n的升序排列,那么第二个序列的子序列 也是第一个序列的子序列的充要条件 是 它是一个上升的序列
那么问题就变成了求第二个序列的LIS
用上述方法可以解决
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> using namespace std; inline int read() { int f=1,x=0; char ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while(isdigit(ch)) {x=x*10+ch-'0'; ch=getchar();} return x*f; } int n; int a[100005],b[100005],f[100005],g[100005]; int main() { int i; n=read(); for(i=1;i<=n;i++) { a[i]=read(); g[a[i]]=i; } for(i=1;i<=n;i++) { b[i]=read(); b[i]=g[b[i]]; f[i]=0x7fffffff; } f[1]=b[1]; int len=1,l,r,mid; for(i=2;i<=n;i++) { if(b[i]>f[len]) f[++len]=b[i]; else { l=0;r=len; while(l<=r) { mid=(l+r)>>1; if(f[mid]>b[i]) r=mid-1; else l=mid+1; } f[l]=min(f[l],b[i]); } } printf("%d\n",len); return 0; }
~祝大家学习信息学顺利~
BJOI 加油!