最长子序列(线性DP)学习笔记
子序列和子串不一样。子串要求必须连续,而子序列不需要连续。
比如说\(\{a_1,a_2\dots a_n\}\),他的子串就是\(\{a_i,a_{i+1},\dots, a_j|1\leq i\leq j\leq n\}\),而子序列就是\(\{a_{t_1},a_{t_2}\dots a_{t_i}|t_1<t_2<\dots<t_n \}\)只要子序列里面元素的顺序仍然保持原序列里面的顺序就可以了,不要求连续
最长上升子序列
最长上升/不下降/下降/不上升子序列
这四个序列本质都是一样的……理解了本质就可以写出来了
这里用最长上升序列LIS(Longest Increasing Subsequence)来讨论
LIS,顾名思义,就是子序列里面要求严格上升,就是严格升序排列,连相同的都不能有
\(\Theta(n^2)\) DP
很容易想到线性DP,设\(f_i\)表示\(a_i\)结尾的最长的LIS的长度,那么就可以得到
如果没有找到\(a_j<a_i\)呢?那么\(f_i\)就只有\(a_i\)自己,就是1
所以我们可以初始化\(f_i\)全部为1,然后对于\(n\)个\(i\),我们都遍历所有的\(j<i\)如果满足\(a_j<a_i\),那么\(f_i=\max(f_i,f_j+1)\)
分析时间复杂度,对于\(n\)个\(i\),我们都遍历\(i\)个点判断是否满足,复杂度是二次方级别的,大概就是\(\Theta(n^2)\)
如果要准确算的话……\(\Theta(\displaystyle\sum_{i=1}^ni)=\Theta(\frac{n(n+1)}{2})\),大概就是\(\displaystyle\Theta(\frac{n^2}{2})\)的样子
如果我们看看题目:
P1020 [NOIP1999 普及组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是\(\leq 50000\)的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
1行,若干个整数(个数\(\leq 100000\))
输出格式
2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入输出样例
**输入 #1 **
389 207 155 300 299 170 158 65
输出 #1
6
2
根据即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难可得,第一问这套系统最多拦截的导弹数就是求最长不上升子序列,而第二问,就是求最长不上升子序列的最少个数。
那么第二问,对于每一个最长不上升子序列的末尾,肯定都比下一个数要小,也就是说我们只要求出原序列的最长严格上升序列,就是这个问的答案,因为即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难
所以我们就很开心交了一个代码上去
#include<iostream>
using namespace std;
int h[100001],n=1,b[100001],b1[100001],a1=1,a2=1;
int main()
{
while(cin>>h[n])
{
b[n]=1;b1[n++]=1;
}
n--;
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
// 只要找到前面有不小于我的,就可以把我接在这个的后面形成一个不上升子序列
if(h[j]>=h[i])b[i]=max(b[i],b[j]+1);
}
a1=-1;
for(int i=1;i<=n;i++)
{
// 寻找最大值
if(b[i]>a1)a1=b[i];
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<i;j++)
// 只要找到前面有小于我的,就可以把我接在这个的后面形成一个严格上升子序列
if(h[j]<h[i])b1[i]=max(b1[i],b1[j]+1);
}
a2=-1;
for(int i=1;i<=n;i++)
{
// 寻找最大值
if(b1[i]>a2)a2=b1[i];
}
cout<<a1<<endl<<a2;
return 0;
}
然后你就会发现,这道题\(\Theta(n^2)\) 是可以拿100分的!!!
是啊满分200呢。
考虑优化
\(\Theta(n\log_2n)\) 单调二分优化DP
首先,对于每个点都要跑一次遍历,这里的\(\Theta(n)\)是不能省的了。我们考虑每次遍历时候的\(\Theta(n)\)
我们每次遍历都看了看我能不能接上去,但是就会在接不上去和接上去也亏这里浪费掉很多时间。
那我们就可以设\(dp_i\)表示长度为\(i\)的LIS的最优结尾
为什么说最优结尾呢?如果长度一样,那么肯定是用\(3\)结尾比用\(5\)结尾要好。因为如果后面有4,那么3能接上去而5不能。
然后我们又阔以知道,\(dp\)数组一定是严格单调递增的。
我们设当前的两个LIS,\(a\)长度为\(l\)结尾是\(x\)(即\(dp_l=x\)),\(b\)长度\(l+1\)结尾也是\(x\)(即\(dp_{l+1}=x\))。
所以我们就可以知道\(b_{l+1}=x,b_l<b_{l+1},\therefore b_l<x\)
那么我们长度为\(l\)的LIS的最优结尾\(dp_l\)不就应该等于\(b_{l-1}\)吗?那就小于\(x\)啦。
所以假设是不成立哒~
知道了\(dp\)是单调递增的时候,我们就可以,设当前最长LIS的长度为\(len\),那么\(dp\)数组现在的大小应该也是\(len\),再设现在遍历到了\(a_i\)
-
如果\(a_i>dp_{len}\),那么就说明\(a_i\)可以接在最长LIS的后面,成为下一个最优结尾,即
dp[++len]=a[i]
-
如果\(a_i\leq dp_{len}\),我们也不能就这样舍弃掉\(a_i\),毕竟它可能可以把前面没那么优的结尾 取而代之也。
我们设\(a_i\)将要取代\(dp_j(j<len)\) 如果\(a_i>dp_j\),那么其实这样取代是不划算的。本来人家就比你更有潜力能接更多数,如果换了你,可能你就接不了4了(上文\滑稽
那么如果有一堆都比\(a_i\)大肿么办? 肯定代替掉不小于\(a_i\)的最小的那个
是不是很有二份答案的感觉?
是哒我们就用二分来寻找这个最小,然后总的复杂度就是\(\displaystyle \mathcal O(n\log_2n)\)
我知道你们会有什么问题
这里\(dp_{len}\)表示的只是长度为\(len\)的上升子序列的最优结尾,并不代表任何关于原序列的位置信息
也就是如果我现在递推到\(a_i\),那么\(dp\)表示的就是\(a_1\)到\(a_{i-1}\)的序列的最优结尾的信息。
至于具体的序列是什么……没办法知道。
现在推荐俩个STL
template <class ForwardIterator, class T, class Compare>
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
(完蛋找不到upper_bound原型了)
好了我们推荐一个STL
lower_bound(begin,end,val),是返回地址begin
和地址end
之间的有序序列第一个大于等于 数值 val
的地址
比如说我有数组\(a\),已知这个数组单调不下降,\(a_5=a_6=7\),那么我们调用lower_bound(a,a+n)
就会返回第一个大于等于7的\(a_5\)的地址
然后C++党福利就是指针是阔以相减的。
用返回值减去数组开头就是这个元素在数组中下标。
比如上面,lower_bound(a,a+n)-a=5
至于那个找不到的upper_bound(begin,end,val)
是返回地址begin
和地址end
之间的有序序列第一个严格大于 数值 val
的地址
其他就和lower_bound
一样了
然后这个有序序列
在STL中其实就是默认升序
所以不加上cmp的sort出来是升序
但是如果我们真的需要降序
或者这个数组本来就是降序
那么我们其实可以定义比较函数的。
lower_bound比较器里面只要val比当前的中值(就是二分的中值)“小”就向左跳,否则就向右跳。
所以我们只要在处理降序序列的时候告诉lower_bound,只要(int)a>(int)b,就认为a比b“小”
我:我现在告诉你,lower_bound,5比3小
lower_bound:你现在要找的val是5,中值是3,那么寻找第一个“大于等于”5的数,就应该到左边去找。
这里“大于等于”,在傻傻的lower_bound眼中,就是不满足我们给的"小"。
lower_bound:我帮你在这个有序序列中找到第一个不"小于”5的数
翻译成人话:lower_bound在这个降序序列中找到第一个小于等于5的数
所以我们就可以用lower_bound在单调上升序列\(dp\)中寻找到第一个大于等于\(a_i\)的数,取而代之。因为,LIS是不允许重复的。
但是……emmm如果是不下降子序列呢?
那就找到第一个大于\(a_i\)的呗~
用那个我怎么也找不到的upper_bound
记得初始值\(dp_1=a_1\)
#include <cstdio>
#include <iostream>
#include <algorithm>
int h[100001],cnt;
int dp[100001],len;
int main()
{
while(~scanf("%d",&h[++cnt]));
--cnt;
dp[++len]=h[1];
for(register int i=2;i<=cnt;++i)
{
/// 最长不下降子序列
if(h[i]<=dp[len]) dp[++len]=h[i];
else
{
// 这里dp数组是单调不上升的,所以要用upper_bound
// 又因为它不上升,那就肯定不是升序啦?
// 就要重新定义“小”
// 结构体std::greater<typename>的重载运算符(typename a.typename b)可以返回a>b
// 自己写cmp函数也没可以的
int j=std::upper_bound(dp+1,dp+1+len,h[i],std::greater<int>())-dp;
dp[j]=h[i];
}
}
printf("%d\n",len);
dp[len=1]=h[1];
for(register int i=2;i<=cnt;++i)
{
/// 最长严格上升子序列
if(h[i]>dp[len]) dp[++len]=h[i];
else
{
int j=std::lower_bound(dp+1,dp+1+len,h[i])-dp;
dp[j]=h[i];
}
}
printf("%d\n",len);
return 0;
}
\(\Theta(n\log_2n)\) 树状数组优化
我们考虑LISz中\(f_i\)的转移方程
要求一段区间的最大值……我们可以用线段树。
当然退化一下树状数组更省空间。
这次我们设(注意两个优化的dp数组表示不同含义)\(dp_i\)表示以\(a_i\)结尾的LIS的最长长度
再设\(t_x\)为\(x\)结尾的LIS的最长长度
那么我们求LIS的时候就可以遍历\(t_1\dots t_{a_i-1}\)之间的最大值,然后再用\(a_i\)接上去,最后再更新\(t_{a_i}\)就可以了。
然后我们就可以用树状数组来维护\(t_1\dots t_j\)的最大值
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
int cnt,h[100001],hsh[100001];
int lowbit(int x)
{
return x&(-x);
}
void add(int T[],int i,int x)
{
while(i<=cnt)
{
T[i]=std::max(T[i],x);
i+=lowbit(i);
}
}
int query(int T[],int n)
{
int res=0;
while(n)
{
res=std::max(T[n],res);
n-=lowbit(n);
}
return res;
}
int T[100001],dp[100001];
int ans=0;
int main()
{
while(~scanf("%d",&h[++cnt]));
--cnt;
std::memcpy(hsh,h,sizeof(h));
std::sort(hsh+1,hsh+1+cnt);
int m=std::unique(hsh+1,hsh+1+cnt)-hsh-1;
// 必须离散化,否则t的下标会爆,可以不去重,原序列可以无序
for(register int i=1;i<=cnt;++i) h[i]=std::lower_bound(hsh+1,hsh+1+m,h[i])-hsh;
for(register int i=1;i<=cnt;++i)
{
/// 最长不下降子序列,那就不是升序啦?
/// 但是树状数组的下标维护必须升序,那就考虑用相反数
/// 又但是树状数组的下标必须大于0,所以加上m+1
int j=query(T,m-h[i]+1);
ans=std::max(ans,dp[i]=j+1);
add(T,m-h[i]+1,dp[i]);
}
printf("%d\n",ans);
ans=0;
memset(T,0,sizeof(T));
for(register int i=1;i<=cnt;++i)
{
/// 最长严格上升子序列,所以相同的不能算,树状数组下标要-1,而不是h[i]
int j=query(T,h[i]-1);
ans=std::max(ans,dp[i]=j+1);
add(T,h[i],dp[i]);
}
printf("%d\n",ans);
return 0;
}
然后你就发现dp数组没什么用……用\(j+1\)不就全解决了吗
最长公共子序列
LCS(我也不知道英文是啥)
给定两个数组\(a,b\)求他们的最长公共子序列
(就是文字意思)
设\(dp_{i,j}\)表示\(a_{1\dots i},b_{1\dots j}\)的最长公共子序列,那么就会有
\(\Theta(n^2)\),目前没有想到优化
所以对于题目
题目描述
给出 \(1,2,\ldots,n\) 的两个排列 \(P_1\) 和 \(P_2\) ,求它们的最长公共子序列。
输入格式
第一行是一个数 \(n\)。
接下来两行,每行为 \(n\) 个数,为自然数 \(1,2,\ldots,n\) 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
输入输出样例
输入 #1
5
3 2 1 4 5
1 2 3 4 5
输出 #1
3
说明/提示
- 对于 50% 的数据, \(n \le 10^3\);
- 对于 100% 的数据, \(n \le 10^5\)。
我们可以抱着试一试的心态用\(\Theta(n^2)\)的算法
#include <cstdio>
#include <iostream>
int n;
int dp[100001][100001],a[100001],b[100001];
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;++i) scanf("%d",a+i);
for(register int i=1;i<=n;++i) scanf("%d",b+i);
for(register int i=1;i<=n;++i)
{
for(register int j=1;j<=n;++j)
{
if(a[i]==b[j]) dp[i][j]=std::max(dp[i][j],dp[i-1][j-1]+1);
else dp[i][j]=std::max(dp[i-1][j],dp[i][j-1]);
}
}
printf("%d\n",dp[n][n]);
return 0;
}
然后你就会很开心发现
g++ P1439.cpp -o P1439.exe
P1439.cpp:5:22: error: size of array 'dp' is too large
int dp[100001][100001],a[100001],b[100001];
^
P1439.cpp: In function 'int main()':
P1439.cpp:16:19: error: 'dp' was not declared in this scope
if(a[i]==b[j]) dp[i][j]=std::max(dp[i][j],dp[i-1][j-1]+1);
^
P1439.cpp:17:9: error: 'dp' was not declared in this scope
else dp[i][j]=std::max(dp[i-1][j],dp[i][j-1]);
^
P1439.cpp:20:16: error: 'dp' was not declared in this scope
printf("%d\n",dp[n][n]);
什么意思?
就是dp数组太大了
时间没炸空间先爆
好吧我们注意一下
给出 \(1,2,\ldots,n\) 的两个排列 \(P_1\) 和 \(P_2\)
既然是排列
那么就因该有这两个集合相同,而且元素互异。
我们可以随便找一下样例中的公共子序列
比如3 4 5
然后从上面的3向下面的3连一条线
4也是
5还是
就发现这三条线完全没有交点
找其他公共子序列也是这样
所以我们就发现,当\(P_2\)升序排列时,公共序列的元素在\(P_1\)中的相对位置和在\(P_2\)中的相对位置一样
然后我们把限制条件拿掉
公共序列的元素在\(P_1\)中的相对位置和在\(P_2\)中的相对位置一样
因为这个序列是公共的。
而且这个序列是可以确定的。
那么这个子序列的元素之间相对位置就和在原序列中的相对位置一样
即公共序列的元素在中的相对位置和在中的相对位置一样
然后我们又知道这些元素是互异的。
所以对于\(P_1\)中的每一个元素,我们都可以知道他们在\(P_2\)中的位置,设为\(pos\)。
那么只要\(P_1\)的子序列满足子序列的元素在\(P_2\)中的位置也是有序的,那么这个子序列就是公共子序列
也就是我们要求\(pos\)中的LIS
我管你用什么方法反正空间上卡过去了
时间上\(\Theta(n\log_2n)\)就可以过
#include <cstdio>
#include <algorithm>
int n;
int a[100001],b[100001],hsh[100001],dp[100001],len;
int main()
{
scanf("%d",&n);
for(register int i=1;i<=n;++i) scanf("%d",a+i);
for(register int i=1;i<=n;++i) scanf("%d",b+i);
// 求出元素在b[i]中的位置
for(register int i=1;i<=n;++i) hsh[b[i]]=i;
// 映射回a数组
for(register int i=1;i<=n;++i) a[i]=hsh[a[i]];
dp[++len]=a[1];
for(register int i=2;i<=n;++i)
{
if(a[i]>dp[len]) dp[++len]=a[i];
else
{
int j=std::upper_bound(dp+1,dp+1+len,a[i])-dp;
dp[j]=a[i];
}
}
printf("%d\n",len);
return 0;
}