线性DP

线性DP

今日 \(2025.10.22\),距 \(CSP-J/\) \(10\)天,\(NOIP\) \(38\)

昨天把最短路爆切了,但发现自己的 \(DP\) 还是太弱了,所以我决定 \(CSP\) 剩下的这几天练练 \(DP\)。自己没系统的学过 \(DP\),所以决定从最基础的 线性 \(DP\) 开始学

在这里吐槽一句:DP黄题都比最短路的蓝题难(个人感觉)

总结一下线性DP的几种常见类型:

1.最长上升子序列

2.数塔

3.其他

最长上升子序列:

luogu B3637 最长上升子序列

板题,直接放代码:

时间复杂度:\(O(n^2)\)

#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,a[N],f[N],ans;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		f[i]=1;
		for(int j=1;j<i;j++){
			if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
		}
        ans=max(ans,f[i]);
	}
	cout<<ans;
	return 0;
}

还有 \(O(nlogn)\) 的贪心做法:

#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,len,q[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		int k;
		cin>>k;
		if(len==0||k>q[len-1]) q[len++]=k;
		else{
			int x=lower_bound(q,q+len,k)-q;
			q[x]=k;
		}
	}
	cout<<len;
	return 0;
}

当然,还可以用权值线段树优化什么的,但本文不讨论 \(DP\) 的优化,以上这个 \(O(nlogn)\) 的贪心在后文直接使用了(有更优的复杂度当然要学)

luogu P1091 [NOIP 2004 提高组] 合唱队形

本题只需正反分别求一次最长上升子序列,正向为 \(f[i]\) 反向为 \(g[i]\),最后 \(max(f[i]+g[i])\) 即为答案

#include<bits/stdc++.h>
using namespace std;
const int N=105;
int n,f[N],g[N],a[N],ans;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++){
		f[i]=1;
		for(int j=1;j<i;j++){
			if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
		}
	}
	for(int i=n;i;i--){
		g[i]=1;
		for(int j=n;j>i;j--){
			if(a[i]>a[j]) g[i]=max(g[i],g[j]+1);
		}
	}
	for(int i=1;i<=n;i++) ans=max(ans,f[i]+g[i]-1);
	cout<<n-ans;
	return 0;
}

luogu P2285 [HNOI2004] 打鼹鼠

本题开始都是通过巧妙转换变为最长上升子序列的题,有几类常见转换方法,在后面几道题中会一一体现

根据题意:第 \(i\) 只鼹鼠只会在 \(t_i\) 时出现,如果在此时刻没打死它,在下一时刻它就会消失,并且,在 \(t_i\) 之前鼹鼠不会出现,是打不到的。

首先,有一个易发现的性质:初始时刻站到某一只鼹鼠的坑位上,结果一定不比初始时在空白坑位差。很好理解,我直接站到坑位上,就省去了到第一只鼹鼠坑位所移动的时间。

接着,对于其中两只鼹鼠,只有时间到下一只鼹鼠出现的时候,才能打它,导致我就算提前到了下一只鼹鼠的坑位,也得等到它出现的时刻,才能打死下一只鼹鼠,这对于打死下下只鼹鼠来说,时间差就不会因为提前到下一只鼹鼠而改变。所以,对于任意两只鼹鼠,记两只鼹鼠的曼哈顿距离为 \(d\),时间之差为 \(\Delta t\),不难发现只有当 \(d≤\Delta t\) 时,才能打死下一只鼹鼠

不难发现,这里的 \(d≤\Delta t\) 和最长上升子序列的板题中的决策很像(事实上就是一样的),所以,这道题其实就转换成了,类似于求最长上升子序列的题(注意,题目保证了每只鼹鼠的出现时间是递增的,所以按出现时间排序,否则还得按出现时间排序)

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int s,n,f[N],ans;
struct mouse{
	int t,x,y;
}m[N];
int d(int x1,int y1,int x2,int y2){
	return abs(x1-x2)+abs(y1-y2);
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>s>>n;
	for(int i=1;i<=n;i++) cin>>m[i].t>>m[i].x>>m[i].y;
	for(int i=1;i<=n;i++){
		f[i]=1;
		for(int j=1;j<i;j++){
			if(m[i].t-m[j].t>=d(m[i].x,m[i].y,m[j].x,m[j].y)) f[i]=max(f[i],f[j]+1);
		}
		ans=max(ans,f[i]);
	}
	cout<<ans;
	return 0;
}

luogu P2782 友好城市

直接说思路:

记第 \(i\) 对两岸的城市编号分别为 \(a[i]\)\(b[i]\)

先把每对城市按 \(a[i]\) 升序排序,不难发现,如果我选了 第 \(i\) 对城市,那么如果要选第 \(j\) 对城市(\(j>i\)),必须要 \(b[j]>b[i]\),否则就会有交叉,就不可以批准了。到这里都已经成了 \(LIS\) 裸题了

代码:

//本题n为2e5,所以用贪心法求LIS
#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
const int N=2e5+5;
int n,q[N],len;
pair<int,int>p[N];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>p[i].x>>p[i].y;
	sort(p+1,p+1+n);
	for(int i=1;i<=n;i++){
		if(len==0||p[i].y>q[len-1]) q[len++]=p[i].y;
		else{
			int x=lower_bound(q,q+len,p[i].y)-q;
			q[x]=p[i].y;
		}
	}
	cout<<len;
	return 0;
}

P1233 木棍加工

本题依旧按某一个参数排序,按 \(l\) 或者 \(w\) 都可以,要降序排序,然后,对另一个参数求 \(LIS\) 即可。

注意,可能会有疑问,求的最小时间,为什么答案是 \(LIS\)?这里的最小的分钟数是由降序排序保证的,而求的 \(LIS\) 是加工至少必须花的时间

#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int n,q[N],len;
struct node{
	int l,w;
}a[N];
bool cmp(node a,node b){
	if(a.l==b.l) return a.w>b.w;
	return a.l>b.l;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].l>>a[i].w;
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;i++){
		if(len==0||a[i].w>q[len-1]) q[len++]=a[i].w;
		else{
			int x=lower_bound(q,q+len,a[i].w)-q;
			q[x]=a[i].w;
		}
	}
	cout<<len;
	return 0;
}

以上两题的转化思路是:有双参数,按其中一个参数升序或降序排序

数塔

luogu P1216 [IOI 1994 / USACO1.5] 数字三角形 Number Triangles

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,f[N],ans;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=n-i+1;j<=n;j++){
			int a;
			cin>>a;
			f[j]=max(f[j],f[j+1])+a;
		}
	}
	for(int i=1;i<=n;i++) ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}

有一点要说的,求组合数 \(C^{m}_{n} = C^{m}_{n-1} + C^{m-1}_{n-1}\),不难发现,就是数塔,并且,数塔貌似也就求组合数用的多一些,掌握组合数代码就好了。

其他

接着才是重头戏:

类似背包转移:

luogu P1077 [NOIP 2012 普及组] 摆花

思路:

设计状态:\(f_{i,j}\) 表示到第 \(i\) 朵花为止,已经用了 \(j\) 个花瓶的总方案数。

转移:

\[f_{i,j} = \Sigma_{k=0}^{min(a_i,j)} f_{i-1,j-k} \]

答案:\(f_{n,m}\)

初始化:\(f_{0,0}=1\)

#include<bits/stdc++.h>
using namespace std;
const int N=105,mod=1e6+7;
int n,m,f[N][N],a[N];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			for(int k=0;k<=a[i];k++){
				if(j>=k) f[i][j]=(f[i][j]+f[i-1][j-k])%mod;
			}
		}
	}
	cout<<f[n][m];
	return 0;
}

luogu P1854 [IOI 1999] 花店橱窗布置

几乎一样,只不过从方案数变成了最优解,这题要输出一种方案,用一个 \(pre\) 数组记录一下,\(f\) 由谁转移,最后递归输出即可

#include<bits/stdc++.h>
using namespace std;
const int N=105;
int n,m,a[N][N],f[N][N],pre[N][N];
void dfs(int t,int id){
	if(t==0) return;
	dfs(t-1,pre[t][id]);
	cout<<id<<" ";
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>a[i][j];
            f[i][j]=-0x3f3f3f3f;
		}
	}
    f[0][0]=0;
	for(int i=1;i<=n;i++){
		for(int j=i;j<=m;j++){
			for(int k=i-1;k<j;k++){
				if(f[i][j]<f[i-1][k]+a[i][j]){
					f[i][j]=f[i-1][k]+a[i][j];
					pre[i][j]=k;
				}
			}
		}
	}
	int ans=-0x3f3f3f3f,id;
	for(int i=n;i<=m;i++){
		if(ans<f[n][i]){
			ans=f[n][i];
			id=i;
		}
	}
	cout<<ans<<endl;
	dfs(n,id);
	return 0;
}

luogu P2066 机器分配

以及一样的题,这题输出的方案要按编号递增的公司分的机器单调不降的输出,此时,\(pre\) 数组只需在 \(f_{i,j}<=f_{i-1,j-k}+a_{i,k}\) 记录即可("\(=\)"就是在盈利一样时,把方案更新成符合要求的)

#include<bits/stdc++.h>
using namespace std;
int n,m,a[11][16],f[11][16],pre[11][16];
void dfs(int i,int j){
	if(i==0) return;
	dfs(i-1,j-pre[i][j]);
	cout<<i<<" "<<pre[i][j]<<endl;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>a[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int k=0;k<=j;k++){
				if(f[i][j]<=f[i-1][j-k]+a[i][k]){
					f[i][j]=f[i-1][j-k]+a[i][k];
					pre[i][j]=k;
				}
			}
		}
	}
	cout<<f[n][m]<<endl;
	dfs(n,m);
	return 0;
}
posted @ 2025-10-23 08:02  czh(抽纸盒)  阅读(4)  评论(0)    收藏  举报