[笔记]线性dp常见模型及拓展
本文主要用于记录\(dp\)学习中的一些线性模型(模板问题讲解较少,只有结论性内容和代码,而拓展会有较详细的讲解)。
\(dp\)的线性模型指的是状态转移有明显线性顺序(如一维二维数组、队列、栈等)的\(dp\),包括背包问题也是线性\(dp\)。
具体定义见https://blog.csdn.net/qq_33164724/article/details/104428502。
🍎 最长上升子序列(LIS)
LIS 最长上升子序列 / LNDS 最长不下降子序列 / LDS 最长下降子序列 / LNIS 最长不上升子序列
用\(f[i]\)表示长度为\(i\)的上升子序列,最后一个元素的最小值,其长度为\(len\),初始为\(0\)。
- 如果\(f\)为空,则直接加入该元素。
- 如果该元素\(>f[len]\),则直接加入到\(f\)的最后,\(len\)加\(1\)。
- 如果该元素\(\leq f[len]\),则在前面找第一个\(\geq f[len]\)的位置,修改为当前元素。
注意到\(f\)是单调的,所以查找这一步可以用二分,总时间复杂度\(O(nlogn)\)。
注:如果要找最长不下降子序列,就在该元素\(\geq f[len]\)时直接加入,否则找第一个\(>f[len]\)的修改。
Code
LIS 最长上升子序列(输出长度)
#include<bits/stdc++.h> using namespace std; int n,a[5010],f[5010]; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=INT_MIN; int len=0; for(int i=1;i<=n;i++){ if(a[i]>f[len]){ f[++len]=a[i]; }else{ int pos=lower_bound(f+1,f+1+len,a[i])-f; f[pos]=a[i]; } } cout<<len; return 0; }
LNDS 最长不下降子序列(输出长度)
#include<bits/stdc++.h> using namespace std; int n,a[5010],f[5010]; int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=INT_MIN; int len=0; for(int i=1;i<=n;i++){ if(a[i]>=f[len]){ f[++len]=a[i]; }else{ int pos=upper_bound(f+1,f+1+len,a[i])-f; f[pos]=a[i]; } } cout<<len; return 0; }
LDS 最长下降子序列(输出长度)
#include<bits/stdc++.h> using namespace std; int n,a[5010],f[5010]; bool cmp(int a,int b){ return a>=b; } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=INT_MAX; int len=0; for(int i=1;i<=n;i++){ if(a[i]<f[len]){ f[++len]=a[i]; }else{ int pos=lower_bound(f+1,f+1+len,a[i],cmp)-f; f[pos]=a[i]; } } cout<<len; return 0; }
LNIS 最长不上升子序列(输出长度)
#include<bits/stdc++.h> using namespace std; int n,a[5010],f[5010]; bool cmp(int a,int b){ return a>b; } int main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; f[0]=INT_MAX; int len=0; for(int i=1;i<=n;i++){ if(a[i]<=f[len]){ f[++len]=a[i]; }else{ int pos=upper_bound(f+1,f+1+len,a[i],cmp)-f; f[pos]=a[i]; } } cout<<len; return 0; }
🍐 最大连续子段和
用\(cur\)记录下到当前这一个连续段的和,\(ans\)记录最大值。
遍历每一个数值,如果说\(cur+a[i]<a[i]\),说明前面的选上只会徒增负担,故舍弃,从\(a[i]\)开始一个新的子段。每次结束后更新\(ans\)即可。
注:不需要用数组存,现读现算即可。
思考:长度最少为$2$的最大连续子段和怎么求呢?
一样的思路,只需要每次比较两个值,初始值为$a[1]+a[2]$即可。
思考:长度最少为$k$的最大连续子段和怎么求呢?
还是一样的思路,只需要每次比较$k$个值,初始值为$a[1]+a[2]+……+a[k]$即可。
需要注意的是,如果遍历$k$个元素的和,会导致无法$O(n)$完成。因此我们需要维护一个前缀和。
拓展:如果不是取$1$段,而是$m$段,又该怎么办呢?
详见此文。
Code
最大连续子段和
#include<bits/stdc++.h> using namespace std; int n,a,ans=INT_MIN; int main(){ cin>>n; int cur; for(int i=1;i<=n;i++){ cin>>a; if(i==1) cur=a; else cur=max(a,cur+a); ans=max(ans,cur); } cout<<ans; return 0; }
长度至少为$2$的最大连续子段和
#include<bits/stdc++.h> using namespace std; int n,a[200010],dp[200010]; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } int cur=a[1]+a[2],ans=cur; for(int i=3;i<=n;i++){ cur+=a[i]; if(cur<a[i]+a[i-1]) cur=a[i]+a[i-1]; if(cur>ans) ans=cur; } cout<<ans; return 0; }
长度至少为$k$的最大连续子段和
#include<bits/stdc++.h> using namespace std; int n,k,a[200010],b[200010],dp[200010]; int main(){ cin>>n>>k; for(int i=1;i<=n;i++){ cin>>a[i]; b[i]=b[i-1]+a[i]; } int cur=b[k],ans=cur; for(int i=k+1;i<=n;i++){ cur+=a[i]; if(cur<b[i]-b[i-k]) cur=b[i]-b[i-k]; if(cur>ans) ans=cur; } cout<<ans; return 0; }
🥭 最大上升子序列和
用\(f[i]\)表示到第\(i\)个元素的最大上升子序列和。
遍历每一个元素。对于每一个位置,遍历其前面所有的元素,如果遇到\(a[j]<a[i]\)的,就用\(f[j]\)更新最大值。
最后别忘了加上本身\(a_i\)。
Code
最大上升子序列和
#include<bits/stdc++.h> using namespace std; int n,a[100010],f[100010],ans=INT_MIN; int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } for(int i=1;i<=n;i++){ for(int j=1;j<i;j++){ if(a[i]>a[j]) f[i]=max(f[i],f[j]); } f[i]+=a[i]; ans=max(ans,f[i]); } cout<<ans<<endl; return 0; }
🫐 最长公共子序列(LCS)
用\(f[i][j]\)表示\(A[1\sim i]\)和\(B[1\sim j]\)的LCS长度。递推公式:
最终答案就是\(f[n][m]\)。
若要输出路径(如Atcoder dp_f LCS),需要用另一个二维数组记录上一个格子在上方、左方还是左上方。从最右下角开始回溯,遇到往左上方走的就添加\(a[i]\)或\(b[j]\)(此时它们是相等的)入\(ans\)字符串。最后反序输出即可。如下图(网上找的):
拓展:有$O(m^2\log n)$和$O(m^2+n\Sigma)$($\Sigma$是字符集大小)的做法。
对于长度为$n$的$S$与长度为$m$的$T$,设$f[i][j]$为考虑$T$的前$i$个元素,LCS长度为$j$时,$S$匹配到的最小下标,若匹配不到为$+\infty$。
则\(f[i][j]\)可以被\(f[i-1][j]\)和\(k\)更新,其中\(k\)是\((f[i-1][j-1]+1)\sim n\)中满足\(S[x]=j\)的最小\(x\),可以二分求出。这样时间复杂度是\(O(m^2 \log n)\)。
我们花费\(O(n\Sigma)\)的复杂度来维护一个\(nxt[i][j]\),表示\(i\sim n\)中满足\(S[x]=j\)的最小\(x\)。转移时就不用二分了,直接调用\(nxt\)即可,时间复杂度\(O(m^2+n\Sigma)\)。
这样我们可以应对\(m\)较小,\(n\)较大的情况,具体可以根据字符集大小来决定使用哪一种,字符集超过\(m\log n\)使用前者。
听说这个叫序列自动机?
点击查看代码
#include<bits/stdc++.h> #define N 1000010 #define M 1010 #define C 26 using namespace std; string s,t; int n,m,f[M][M],cur[C],nxt[N][C]; //f[i][j]:T匹配到i,LCS长为j,S最少用到哪里 signed main(){ cin>>s>>t; n=s.size(),m=t.size(),s=' '+s,t=' '+t; memset(cur,0x3f,sizeof cur); for(int i=0;i<C;i++) nxt[n+1][i]=cur[0]; for(int i=n;i>=1;i--){ cur[s[i]-'a']=i; for(int j=0;j<C;j++) nxt[i][j]=cur[j]; } memset(f,0x3f,sizeof f); int INF=f[0][0]; for(int i=0;i<=m;i++) f[i][0]=0; for(int i=1;i<=m;i++){ for(int j=1;j<=i;j++){ f[i][j]=f[i-1][j]; if(f[i-1][j-1]!=INF) f[i][j]=min(f[i][j],nxt[f[i-1][j-1]+1][t[i]-'a']); } } for(int i=m;i>=1;i--){ if(f[m][i]!=INF){ cout<<i<<"\n"; return 0; } } return 0; }
拓展:还有
bitset
压位解法。时间复杂度是$O(\frac{nm}{\omega})$,$\omega$为字长。这个还没学,就先不放了(逃
拓展:没有重复元素且两序列元素集合相同时可以转化为LIS问题。
详见此文。
拓展:如何计算LCS个数?
详见此文。
Code
LCS输出路径
#include<bits/stdc++.h> using namespace std; string a,b; int n,m,f[3010][3010]; char d[3010][3010]; int main(){ cin>>a>>b; n=a.size(),m=b.size(); a=' '+a,b=' '+b; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(a[i]==b[j]){ d[i][j]='+'; f[i][j]=f[i-1][j-1]+1; }else{ if(f[i-1][j]>f[i][j-1]){ d[i][j]='U'; f[i][j]=f[i-1][j]; }else{ d[i][j]='L'; f[i][j]=f[i][j-1]; } } } } string ans=""; for(int x=n,y=m;x>0&&y>0;){ if(d[x][y]=='+'){ ans+=a[x]; x--,y--; }else if(d[x][y]=='U'){ x--; }else{ y--; } } reverse(ans.begin(),ans.end()); cout<<ans; return 0; }
\([Fin.]\)
如果有任何建议或疑问,请在评论区告诉我,我会不断改进。谢谢!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效