【学习笔记】 dp笔记

好久没写 \(dp\) 老师却开了一场 \(dp\) 基础场 挂的很惨……

  • T1 Acwing271

观察到 \(k\) 的范围很小。那么我们可以把每一层的状态全部表示出来:

\(dp_{i,j,k,l,m}\) 表示五层 按顺序 分别放了 \(i,j,k,l,m\) 个人的方案数。

由于我们已经按顺序放了人数,那么我们现在只需要考虑每一个人放的前面和上面有没有人。

因为是按照顺序放的,那他前面和上面的人一定满足单调。

所以方程就变得简单了:每一维度看一看它能不能放一个人上去即可。

注意题目给的人数顺序,以及枚举的时候枚举上界不得超过其上一层的人数,否则必然不会被转移。

复杂度最大是 \(O(6^5)\).

#include<bits/stdc++.h>
using namespace std;
#define int long long
int k;
inline int Min(int x,int y){return x<y?x:y;}
namespace Hws{
	int s[10];
	int dp[31][31][31][31][31];
	void solve(){
		if(!k)return;
		for(int i=1;i<=k;++i)scanf("%lld",&s[i]);
		for(int i=k+1;i<=5;++i)s[i]=0;
//		reverse(s+1,s+6);
		dp[0][0][0][0][0]=1;
		for(int i=0;i<=s[1];++i)
			for(int j=0;j<=Min(s[2],i);++j)
				for(int k=0;k<=Min(s[3],j);++k)
					for(int l=0;l<=Min(s[4],k);++l)
						for(int m=0;m<=Min(s[5],l);++m){
							if(i>0&&i-1>=j)dp[i][j][k][l][m]+=dp[i-1][j][k][l][m];
							if(j>0&&j-1>=k)dp[i][j][k][l][m]+=dp[i][j-1][k][l][m];
							if(k>0&&k-1>=l)dp[i][j][k][l][m]+=dp[i][j][k-1][l][m];
							if(l>0&&l-1>=m)dp[i][j][k][l][m]+=dp[i][j][k][l-1][m];
							if(m>0)dp[i][j][k][l][m]+=dp[i][j][k][l][m-1];
						}
		printf("%lld\n",dp[s[1]][s[2]][s[3]][s[4]][s[5]]);
		for(int i=0;i<=s[1];++i)
			for(int j=0;j<=s[2];++j)
				for(int k=0;k<=s[3];++k)
					for(int l=0;l<=s[4];++l)
						for(int m=0;m<=s[5];++m)
							dp[i][j][k][l][m]=0;
	}
}
signed main(){
	do{
		scanf("%lld",&k);
		Hws::solve();
	}while(k!=0);
	return 0;
}
  • T2. AcWing272

最长公共上升子序列。

如果强制定义 \(dp[i][j]\)\(A\)\(i\) 结尾 \(B\)\(j\) 结尾的最长公共上升子序列长度 可以用树套树做到 \(O(n^2\log n)\)

改一下定义:设 \(dp[i][j]\)\(A\) 中前 \(i\) 项与 \(B_j\) 匹配的最长公共上升子序列长度。考虑转移:

\(B_j=A_i\) 则需要找到最大的 \(dp[i'][j']\) 使得 \(B_{j'} < B_j.\)

已知 \(A_i=B_j\) 可以替换式子变成 \(B_{j'}<A_i.\)

\(A_i\) 是固定的,故而可以对 \(A_i\) 维护一个合法的 \(dp\) 最大值。

\(B_j \not =A_i\) 则直接继承状态 \(dp[i][j]=dp[i-1][j]\)

注意 \(dp\) 过程中随时更新 \(A_i\) 维护的最大值。

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

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5000;
int n,A[MAXN],B[MAXN];
int dp[MAXN][MAXN];
inline int Max(int x,int y){return x>y?x:y;}
int main(){
	freopen("lics_10.in","r",stdin);
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%d",&A[i]);
	for(int i=1;i<=n;++i)scanf("%d",&B[i]);
	for(int i=1;i<=n;++i){
		int maxn=0;
		for(int j=1;j<=n;++j){
			if(A[i]!=B[j])dp[i][j]=dp[i-1][j];
			else dp[i][j]=maxn+1;
			if(B[j]<A[i])maxn=Max(maxn,dp[i][j]);
		}
	}
	int ans=-1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			ans=Max(ans,dp[i][j]);
	cout<<ans<<endl;
	return 0;
}
  • T3.AcWing273

先猜结论: \(B\) 的取值一定是 \(A\) 中的数。

显然。

考虑设计状态:\(dp[i][j]\) 表示将 \(B[j]\) 设为 \(A[i]\) 并将 \([1,j]\) 全部变成不讲序列的最小代价。

那么 \(dp[i][j]\) 需要由 \(dp[i'][j-1]\) 转移而来。

注意我们 \(dp\) 的是第一个选择 \(A_i\) 的分界点,所以 \(j-1\) 选择的不是 \(A_i.\)

那么 \(dp[i][j]=\min\left\{ dp[k][j-1]\right\}(A_k\leq A_i)\)

所以可以对 \(A_i\) 维护一个小于等于它的最大 \(dp\) 值。

注意维护的是上一层的值,不要在这一层里面更新它。

至于如何维护前缀 \(\min\) 我们可以对单点做完 \(\min\) 后以值域为标做前缀取 \(\min.\)

由于 \(A\) 很大 我们需要离散化;还有,序列 \(A\) 中有重复,所以每一次的 \(minn[a[i]]\) 都要取 \(\min.\)

其他的将序列反过来复制一下代码就好了。

#include<bits/stdc++.h>
using namespace std;
const int dyx=(1<<30);
int dp[2001][2001],n;
int b[5000],blen;
int a[5000],minn[5000];
inline int Min(int x,int y){return x<y?x:y;}
inline int Max(int x,int y){return x>y?x:y;}
inline int Abs(int x){if(x<0)x=-x;return x;}
inline int getpos(int x){
	return lower_bound(b+1,b+blen+1,x)-b;
}
int ans=dyx;
int main(){
	freopen("grading.1.in","r",stdin);
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%d",&a[i]),b[i]=a[i];
	sort(b+1,b+n+1);
	blen=unique(b+1,b+n+1)-b-1;
//	for(int i=1;i<=n;++i)printf("%d ",a[i]);
//	puts("");
	for(int i=1;i<=n;++i)a[i]=getpos(a[i]);
//	for(int i=1;i<=n;++i)printf("%d ",a[i]);
//	puts("");
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j)dp[i][j]=minn[a[j]]+Abs(b[a[j]]-b[a[i]]);
		for(int j=1;j<=n;++j)minn[a[j]]=dyx;
		for(int j=1;j<=n;++j)minn[a[j]]=Min(minn[a[j]],dp[i][j]);
		for(int j=2;j<=n;++j)minn[j]=Min(minn[j],minn[j-1]);
	}
//	for(int i=1;i<=n;++i)
//		for(int j=1;j<=n;++j)
//			printf("%d%c",dp[i][j],j==n?'\n':' ');
	for(int i=1;i<=n;++i)ans=Min(ans,dp[n][i]);
//	cout<<ans<<endl;
	for(int i=1;i<=n;++i){
		minn[i]=0;
		for(int j=1;j<=n;++j)
			dp[i][j]=0;
	}
	reverse(a+1,a+n+1);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j)dp[i][j]=minn[a[j]]+Abs(b[a[j]]-b[a[i]]);
		for(int j=1;j<=n;++j)minn[a[j]]=dyx;
		for(int j=1;j<=n;++j)minn[a[j]]=Min(minn[a[j]],dp[i][j]);
		for(int j=2;j<=n;++j)minn[j]=Min(minn[j],minn[j-1]);
	}
//	for(int i=1;i<=n;++i)
//		for(int j=1;j<=n;++j)
//			printf("%d%c",dp[i][j],j==n?'\n':' ');
	for(int i=1;i<=n;++i)ans=Min(ans,dp[n][i]);
	printf("%d\n",ans);
	return 0;
}
  • T4.AcWing274

确实是考场设的状态……

\(dp[x][y][z]\) 表示第 \(x\) 个询问,有两个点的坐标是 \(x,y\) 的最小代价。第三坐标在 \(pre-query-pos\) 上。

考虑刷表,因为 \(dp[x][y][z]\) 的前驱状态不好枚举。用当前状态去更新未来状态:

\((x,y)\to(q[i],y),(x,y)\to (x,q[i]),(x,y)\to(x,y)\) 三种对应 \(x,y,z\) 跳跃到当前询问点的答案。

于是这题做完了。注意 \(x,y,q[i]\) 不能相等,否则不合法。

#include<bits/stdc++.h>
using namespace std;
const int dyx=0x3f3f3f3f; 
int L,m,a[201][201],q[5000];
int dp[1001][201][201];
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
int main(){
	freopen("4mob.in","r",stdin);
//	freopen("4mob.out","w",stdout);
	scanf("%d%d",&L,&m);
	for(int i=1;i<=L;++i)	
		for(int j=1;j<=L;++j)
			scanf("%d",&a[i][j]);
	q[0]=3;		
	for(int i=1;i<=m;++i)scanf("%d",&q[i]);
	memset(dp,0x3f,sizeof dp);
	dp[0][1][2]=0;
	for(int i=0;i<m;++i){
		for(int j=1;j<=L;++j){
			for(int k=1;k<=L;++k){
				int x=j,y=k;
				if(x==y||y==q[i]||x==q[i])continue;
				dp[i+1][x][y]=Min(dp[i][x][y]+a[q[i]][q[i+1]],dp[i+1][x][y]);
				dp[i+1][q[i]][y]=Min(dp[i][x][y]+a[x][q[i+1]],dp[i+1][q[i]][y]);
				dp[i+1][x][q[i]]=Min(dp[i][x][y]+a[y][q[i+1]],dp[i+1][x][q[i]]);
			}
		}
	}
	int ans=dyx;
	for(int i=1;i<=L;++i)
		for(int j=1;j<=L;++j){
			if(i==j||i==q[m]||j==q[m])continue;
			ans=Min(ans,dp[m][i][j]);
		}
	cout<<ans<<endl;
	return 0;
}
  • T5.传纸条

考虑四维 \(dp\) 以两个点坐标为状态即可。要使得路径不相交,可以让第二个点的 \(j\) 坐标始终大于第一条路的 \(j\) 坐标。复杂度可以 \(O(n^4).\)

注意这样处理之后两条路是走不到终点的,应该在它们到达终点的前一个点输出答案。

#include<bits/stdc++.h>
using namespace std;
int n,m,a[100][100];
int dp[51][51][51][51];
inline int Max(int x,int y){return x>y?x:y;}
inline int Max(int A,int B,int C,int D){
	int q[4];
	q[0]=A;q[1]=B;q[2]=C;q[3]=D;
	sort(q,q+4);
	return q[3];
}
int main(){
//	freopen("5czt.in","r",stdin);
//	freopen("5czt.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			scanf("%d",&a[i][j]);
	dp[1][1][1][1]=0;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			for(int k=1;k<=n;++k){
				for(int l=j+1;l<=m;++l)
				dp[i][j][k][l]=Max(dp[i-1][j][k-1][l],dp[i-1][j][k][l-1],dp[i][j-1][k-1][l],dp[i][j-1][k][l-1])+a[i][j]+a[k][l];
			}
	cout<<dp[n][m-1][n-1][m];
	return 0;
}
  • T6.AcWing278

\(dp[i][j]\) 表示前 \(i\) 个数选出的数的和为 \(j\) 的情况。

三重循环暴力转移即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,a[500];
ll dp[101][10001];
int main() {
	freopen("6szzh.in","r",stdin);
	freopen("6szzh.out","w",stdout);
	cin>>n>>m;
	for(int i=1; i<=n; ++i)cin>>a[i];
	dp[1][a[1]]=dp[1][0]=1;
	for(int i=1;i<=n;++i){
		for(int j=1;j<i;++j){
			for(int k=a[i];k<=m;++k){
				dp[i][k]+=dp[j][k-a[i]];
			}
		}
	}
	ll ans=0;
	for(int i=1; i<=n; ++i)ans+=dp[i][m];
	cout<<ans<<endl;
	return 0;
}
  • T7.AcWing279

考场想到完全背包了奈何人菜没写出来 真是个 sb)

考虑将一个数当做物品, \(N\) 作为容量。

枚举 \(1\to N-1\) 作为物品,再正序枚举体积 \(j\) 即可。这里是直接累加方案。

注意取模数不是 \(unsigned int\)

时间复杂度 \(O(N^2).\)

#include<bits/stdc++.h>
using namespace std;
const int mod=1ll<<31;
int n;
long long dp[5000];
inline int Max(int x,int y){return x>y?x:y;}
int main(){
//	freopen("7lun.in","r",stdin);
//	freopen("7lun.out","w",stdout);
	scanf("%d",&n);
	dp[0]=1;
	for(int i=1;i<n;++i)
		for(int j=i;j<=n;++j)
			dp[j]+=dp[j-i],dp[j]%=mod;
	cout<<dp[n]<<endl;
	return 0;
}
  • T8.AcWing280

待补。

  • T9.AcWing281

一眼的 \(\text{bitset}\) 优化暴力 \(dp\) 以及正解二进制分组背包,然而二进制不太会写了以及看到多组数据没敢写暴力没想到暴力可以过)

\(dp[i]\) 表示 \(i\) 能不能被表示出来,每次更新直接枚举物品更新即可。因为 \(\text{bitset}\) 的常数是 \(\frac{1}{w}.\)

#include<bits/stdc++.h>
using namespace std;
int n,m;
inline int read(){
	int s=0;
	char ch=getchar();
	while(!isdigit(ch))ch=getchar();
	while(isdigit(ch))s=s*10-48+ch,ch=getchar();
	return s;
}
namespace Hws{
	bitset<100010>f;
	int a[100010],c[100010];
	void solve(){
		f.reset();
		if(n==0&&m==0)return;
		for(int i=1;i<=n;++i)a[i]=read();
		for(int i=1;i<=n;++i)c[i]=read();
		f[0]=1;
		for(int i=1;i<=n;++i){
			for(int j=1;j<=c[i];++j)f|=f<<a[i];
		}
		int ans=0;
		for(int i=1;i<=m;++i)if(f[i]==1)ans++;
		cout<<ans<<endl;
	}
}
using namespace Hws;
int main(){
	freopen("coin_part.in","r",stdin);
	freopen("my.out","w",stdout);
	do{
		n=read();m=read();
		solve();
	}while(n!=0&&m!=0);
}
  • T10.AcWing282

区间 \(dp\) 板子,枚举长度后枚举左端点再枚举右端点即可。

#include<bits/stdc++.h>
using namespace std;
const int dyx=(1<<30);
typedef long long ll;
int n,a[500];
ll dp[500][500],sum[500];
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
int main(){
	freopen("10stone.in","r",stdin);
	freopen("10stone.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%d",&a[i]);
	for(int i=1;i<=n;++i)sum[i]=sum[i-1]+a[i];
	for(int i=1;i<=n;++i)
		for(int j=i;j<=n;++j)
			dp[i][j]=dyx;
	for(int i=1;i<=n;++i)dp[i][i]=0;
	for(int len=2;len<=n;++len){
		for(int l=1;l<=n-len+1;++l){
			for(int k=l;k<l+len-1;++k){
				int r=l+len-1;
				dp[l][r]=Min(dp[l][r],dp[l][k]+dp[k+1][r]+sum[r]-sum[l-1]);
			}
		}
	}
	cout<<dp[1][n]<<endl;
	return 0;
}
  • T11. Polygon

原题,也算是板子。(只是之前看到原题一直没有写过导致这次考试没做 然而旁边八年级大佬自己口胡+实现出来了Orz)

考虑断环为链之后做区间 \(dp.\) 观察到题目中有负数,所以乘的时候我们不仅仅要转移最大值,最小值之间的乘积也可能作为答案。

所以需要两个 \(dp\) 数组,方程也长得很像。主要细节在于处理读入。最后输出边的序号的话只需要遍历一遍找最大值对应的边即可。

#include<bits/stdc++.h>
using namespace std;
const int inf=(1<<30);
int n,a[1000];
char s[1000];
int f[200][200],g[200][200];
inline int Max(int x,int y){return x>y?x:y;}
inline int Min(int x,int y){return x<y?x:y;}
int ans=-inf;
int main(){
	scanf("%d\n",&n);
	for(int i=1;i<=n;++i){
		scanf("%c %d",&s[i],&a[i]);
		getchar();
		a[i+n]=a[i];
		s[i+n]=s[i];
	}
	for(int i=1;i<=(n<<1);++i)
		for(int j=1;j<=(n<<1);++j)
			f[i][j]=-inf,g[i][j]=inf;
	for(int i=1;i<=(n<<1);++i)f[i][i]=g[i][i]=a[i];
	for(int len=2;len<=n;++len){
		for(int l=1;l<=(n<<1)-len+1;++l){
			int r=l+len-1;
			for(int k=l;k<r;++k){
				if(s[k+1]=='t'){
					f[l][r]=Max(f[l][r],f[l][k]+f[k+1][r]);
					g[l][r]=Min(g[l][r],g[l][k]+g[k+1][r]);
				}
				else if(s[k+1]=='x'){
					f[l][r]=Max(f[l][r],Max(f[l][k]*f[k+1][r],Max(f[l][k]*g[k+1][r],Max(g[l][k]*g[k+1][r],g[l][k]*f[k+1][r]))));
					g[l][r]=Min(g[l][r],Min(f[l][k]*f[k+1][r],Min(f[l][k]*g[k+1][r],Min(g[l][k]*g[k+1][r],g[l][k]*f[k+1][r]))));
				}
			}
		}
	}
	for(int i=1;i<=n;++i)ans=Max(ans,f[i][i+n-1]);
	cout<<ans<<endl;
	for(int i=1;i<=n;++i){
		if(f[i][i+n-1]==ans)printf("%d ",i);
	}
	return 0;
}
  • T12.AcWing284

看着就很区间 \(dp\) 对吧。然而考试没想写也不太会)

观察到这个东西的性质和 \(dfs\) 序很像:一棵子树对应的一定是一段区间。

那么,对于区间 \([l,r]\) 其中 \([l+1]\) 对应的是进入该子树的点, \([r-1]\) 对应走出该子树的点。

那么考虑怎么样来 \(dp:\) 一般的划分成两个区间当做两棵子树显然不合适,因为题目中没说这是二叉树;

如果把树简单分成两边均包括一堆子树的两部分,这样的答案是重复的:当树长得对称的时候答案就会算重。

所以我们可以考虑:枚举该区间能划分的 第一棵子树 。易得每一次的子树结构都是不一样的,所以这样计数是对的。

枚举区间:若区间两端点颜色不同就可以直接跳过了。

如果相同,考虑将它划分:第一种方案是只有一棵子树,对应区间 \([l+1,r-1]\)

第二种,枚举区间分割点 \(k.\) 在第一种计算后,\(r,r-1\) 都被计算过了,所以 \(k\) 最大只能到 \(r-2\)

那么对应的区间就是:\([l+1,k-1],[k,r]\) 把答案乘起来即可。这里限制 \(k\) 的颜色与端点相同。

之所以是 \(l+1\) 是因为这是进入该子树的点, \(l\) 是该点的父节点;之所以到达了 \(r\) 是因为我们定义枚举的只是第一棵 子树 剩下的统称为其他的,这里其他的应该包含走完所有树内节点,还需要从 \(r\) 走回去。

代码用递推实现。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int mod=1e9;
int dp[301][301];
char s[5000];
int n;
signed main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			dp[i][j]=0;
	for(int i=1;i<=n;++i)dp[i][i]=1;
	for(int len=2;len<=n;++len){
		for(int l=1;l<=n-len+1;++l){
			int r=l+len-1;
			if(s[l]!=s[r])continue;
			dp[l][r]=dp[l+1][r-1]%mod;
			for(int k=l+2;k<=r-2;++k){
				if(s[l]==s[k]){
					dp[l][r]=(dp[l][r]+(dp[l+1][k-1]%mod*dp[k][r]%mod)%mod)%mod;
				}
			}
		}
	}
	cout<<(dp[1][n]%mod)<<endl;
	return 0;
}
posted @ 2021-07-06 17:04  Refined_heart  阅读(65)  评论(1编辑  收藏  举报