[笔记]线性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[i][j]= \begin{cases} 0 & if\ i=0\ or\ j=0\\ f[i-1][j-1]+1 & if\ i,j>0\ and\ a[i]=b[j]\\ max(f[i-1][j],f[i][j-1]) & if\ i,j>0\ and\ a[i]\neq b[j] \end{cases}\]

最终答案就是\(f[n][m]\)

若要输出路径(如Atcoder dp_f LCS),需要用另一个二维数组记录上一个格子在上方左方还是左上方。从最右下角开始回溯,遇到往左上方走的就添加\(a[i]\)\(b[j]\)(此时它们是相等的)入\(ans\)字符串。最后反序输出即可。如下图(网上找的):


拓展:没有重复元素且两序列元素集合相同时可以转化为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.]\)

如果有任何建议或疑问,请在评论区告诉我,我会不断改进。谢谢!

posted @ 2024-03-24 16:41  Sinktank  阅读(78)  评论(0编辑  收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.