最长子序列(线性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\)数组一定是严格单调递增的。
我们设长度为\(l\)的LIS\(a\)结尾是\(x\)(即\(dp_l=x\)),长度为\(l+1\)的LIS\(b\)结尾也是\(x\)(即\(dp_{l+1}=x\)),当然你要设一些小于\(x\)的数,比\(l+1\)还长的LIS也都木有问题哒。
所以我们就可以知道\(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 \Theta(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_j=\max(f_i,t_j)
\]
然后我们就可以用树状数组来维护\(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}\)的最长公共子序列,那么就会有
\begin{cases}
dp_{i-1,j-1}+1 & a_i=b_j\\
\max(dp_{i-1,j},dp_{i,j-1}) & a_i\neq b_j
\end{cases}
\]
\(\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;
}
最长子序列(线性DP)学习笔记的更多相关文章
- 线性DP 学习笔记
前言:线性DP是DP中最基础的.趁着这次复习认真学一下,打好基础. ------------------ 一·几点建议 1.明确状态的定义 比如:$f[i]$的意义是已经处理了前$i个元素,还是处理第 ...
- 数位DP学习笔记
数位DP学习笔记 什么是数位DP? 数位DP比较经典的题目是在数字Li和Ri之间求有多少个满足X性质的数,显然对于所有的题目都可以这样得到一些暴力的分数 我们称之为朴素算法: for(int i=l_ ...
- DP学习笔记
DP学习笔记 可是记下来有什么用呢?我又不会 笨蛋你以后就会了 完全背包问题 先理解初始的DP方程: void solve() { for(int i=0;i<;i++) for(int j=0 ...
- 树形DP 学习笔记
树形DP学习笔记 ps: 本文内容与蓝书一致 树的重心 概念: 一颗树中的一个节点其最大子树的节点树最小 解法:对与每个节点求他儿子的\(size\) ,上方子树的节点个数为\(n-size_u\) ...
- nyoj17-单调递增最长子序列-(dp)
17-单调递增最长子序列 内存限制:64MB 时间限制:3000ms 特判: No通过数:125 提交数:259 难度:4 题目描述: 求一个字符串的最长递增子序列的长度如:dabdbf最长递增子序列 ...
- 斜率优化DP学习笔记
先摆上学习的文章: orzzz:斜率优化dp学习 Accept:斜率优化DP 感谢dalao们的讲解,还是十分清晰的 斜率优化$DP$的本质是,通过转移的一些性质,避免枚举地得到最优转移 经典题:HD ...
- 动态dp学习笔记
我们经常会遇到一些问题,是一些dp的模型,但是加上了什么待修改强制在线之类的,十分毒瘤,如果能有一个模式化的东西解决这类问题就会非常好. 给定一棵n个点的树,点带点权. 有m次操作,每次操作给定x,y ...
- ACM线性基学习笔记
https://www.cnblogs.com/31415926535x/p/11260897.html 概述 最近的几场多校出现了好几次线性基的题目,,会想起之前在尝试西安区域赛的一道区间异或和最大 ...
- java线性表学习笔记(一)
线性表是一种按顺序储存数据是的常用结构,大多数的线性表都支持以下的典型操作: 从线性表提取插入删除一个数据: 找出线性表中的某一个元素: 找出线性表中的元素: 确定线性表中是否包含某一个元素,确定线性 ...
- 动态 DP 学习笔记
不得不承认,去年提高组 D2T3 对动态 DP 起到了良好的普及效果. 动态 DP 主要用于解决一类问题.这类问题一般原本都是较为简单的树上 DP 问题,但是被套上了丧心病狂的修改点权的操作.举个例子 ...
随机推荐
- hyperf从零开始构建微服务(二)——构建服务消费者
阅读目录 构建服务消费者 安装json rpc依赖 安装JSON RPC客户端 server配置 编写业务代码 编写服务消费者类 consumer配置 配置 UserServiceInterface ...
- linux 下的shutdown指令
命令简介: 该命令可以安全关闭或者重新启动系统.你没有看错,shutdown命令不仅可以关闭系统.也可以重启Linux系统. 命令语法: /sbin/shutdown [-t sec] [-arkhn ...
- Powershell 命令行安装 Windows 作业系统
使用 powershell 完全安装或重灌 windows 作业系统的正确姿势 note:完全使用 powershell 指令,绝非在 powershell 终端下键入传统的 cmd 指令使用传统的 ...
- cnblogs-theme-silence 主题设置简约风格
本文参考 更改博客皮肤 更改博客皮肤为Custom 页面定制CSS代码 勾选禁用模板默认CSS 导入复制该文件内容到代码框中 配置代码块复制功能 样式 和 右侧滑动条样式 /*复制功能添加按钮 beg ...
- 大学四年的Python学习笔记分享之一,内容整理的比较多与仔细
翻到以前在大学坚持记录的Python学习笔记,花了一天的时间整理出来,整理时不经回忆起大学的时光,一眨眼几年就过去了,现在还在上学的你们,一定要珍惜现在,有个充实的校园生活.希望这次的分享对于你们有学 ...
- 让tp6显示详细的错误信息及行号
方法一:默认情况下Ttp6不会显示错误信息,在开发环境下想要查看错误信息需要将Config目录下的app.php文件的show_error_msg改成true 但是这样显示的信息也不够完整, 要看到更 ...
- php stream 流封装协议
http://php.net/manual/wrappers.php 过程: 1. 开始通信 2. 读取数据 3. 写入数据 4. 结束通信 usage: * 读写文件系统 <?php $han ...
- 『GoLang』fmt包的使用
目录 1. fmt 包初识 2. 格式化 verb 应用 2.1 通用 2.2 布尔值 2.3 整数 2.4 浮点数与复数 2.5 字符串和 []byte 2.6 指针 2.7 其他 flag 2.8 ...
- php备份mysql 数据库
1.新建php文件 <?phpheader('Content-Type:text/html;charset=utf8'); ini_set("max_execution_time&qu ...
- USACO Section 4
前言 好久没更新这个系列了,最近闲的无聊写一下.有两题搜索懒得写了. P2737 [USACO4.1]麦香牛块Beef McNuggets https://www.luogu.com.cn/probl ...