CSP2025考前恶补Ⅰ:DP

AtCoder Edu DP Contest

题单:AtCoder 的 Educational DP Contest:https://atcoder.jp/contests/dp

A - Frog 1

\(N\) 个台阶。每个台阶编号为 \(1, 2, \ldots, N\)。对于每个 \(i\)\(1 \leq i \leq N\)),第 \(i\) 个台阶的高度为 \(h_i\)

一只青蛙最初在第 \(1\) 个台阶上。青蛙可以重复以下操作,试图到达第 \(N\) 个台阶:

  • 当青蛙在第 \(i\) 个台阶时,可以跳到第 \(i+1\) 或第 \(i+2\) 个台阶。跳到目标台阶 \(j\) 时,需要支付的代价为 \(|h_i - h_j|\)

请你求出青蛙到达第 \(N\) 个台阶所需支付的总代价的最小值。

水题。设 \(dp(i)\) 表示青蛙跳到 \(i\) 时的最小花费,决策是从前一个还是前两个台阶跳过来。

\[dp(i)=\min \left\{dp(i-1)+|h_i-h_{j-1}|,dp(i-2)+|h_i-h_{i-2}|\right\} \]

https://atcoder.jp/contests/dp/submissions/70493499

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n,h[N];
ll dp[N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>h[i];
	dp[1]=0; dp[2]=dp[1]+abs(h[2]-h[1]);
	for(int i=3;i<=n;i++)
	{
		dp[i]=min(dp[i-1]+abs(h[i]-h[i-1]),dp[i-2]+abs(h[i]-h[i-2]));
	}
	cout<<dp[n];
	return 0;
}

B - Frog 2

\(N\) 个台阶。每个台阶编号为 \(1, 2, \ldots, N\)。对于每个 \(i\)\(1 \leq i \leq N\)),第 \(i\) 个台阶的高度为 \(h_i\)

一只青蛙最初站在第 \(1\) 个台阶上。青蛙可以多次进行如下操作,试图到达第 \(N\) 个台阶:

  • 当青蛙在第 \(i\) 个台阶时,可以跳到第 \(i+1, i+2, \ldots, i+K\) 中的任意一个台阶。假设跳到第 \(j\) 个台阶,则需要支付的代价为 \(|h_i - h_j|\)

请你求出青蛙到达第 \(N\) 个台阶所需支付的总代价的最小值。

这次只要把从 \(i-1/i-2\) 个台阶转移过来改成从 \(i-j_{(j \in [1,k])}\) 转移过来就好了。

https://atcoder.jp/contests/dp/submissions/70493592

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n,k;
ll h[N],dp[N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>h[i];
	dp[1]=0;
	for(int i=2;i<=n;i++)
	{
		ll res=1e18;
		for(int j=1;j<=k;j++)
			if(i-j>0) res=min(res,dp[i-j]+abs(h[i]-h[i-j]));
		dp[i]=res;
	}
//	for(int i=1;i<=n;i++) cerr<<dp[i]<<" \n"[i==n];
	cout<<dp[n];
	return 0;
}

C - Vacation

暑假有 \(N\) 天。对于每一天 \(i\)\(1 \leq i \leq N\)),太郎君可以选择以下活动之一:

  • A:在海里游泳,获得幸福度 \(a _ i\)
  • B:在山上抓虫,获得幸福度 \(b _ i\)
  • C:在家做作业,获得幸福度 \(c _ i\)

由于太郎君容易厌倦,他不能连续两天及以上做同样的活动。

请计算太郎君可以获得的最大总幸福度。

如果不考虑活动的选择,直接考虑到第 \(i\) 天的最大价值就会发现有后效性,因为当前选择的活动会影响后面的活动。所以直接设 \(dp(i,\{0,1,2\})\) 表示第 \(i\) 天选择活动 \(A/B/C\) 的最大价值。

https://atcoder.jp/contests/dp/submissions/70495398

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n;
ll a[N],b[N],c[N];
ll dp[N][3];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a[i]; 
		dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b[i]; 
		dp[i][2]=max(dp[i-1][0],dp[i-1][1])+c[i]; 
	}
	cout<<max({dp[n][0],dp[n][1],dp[n][2]});
	return 0;
}
/*
dp[i][0-2]表示第i天选择活动0-2的最大价值 
*/ 

D - Knapsack 1

\(N\) 个物品。每个物品编号为 \(1, 2, \ldots, N\)。对于每个 \(i\)\(1 \leq i \leq N\)),物品 \(i\) 的重量为 \(w_i\),价值为 \(v_i\)

太郎君打算从这 \(N\) 个物品中选择一些,放入背包带回家。背包的容量为 \(W\),所选物品的总重量不能超过 \(W\)

请你求出太郎君能带回家的物品的最大总价值。

  • 所有输入均为整数。
  • \(1 \leq N \leq 100\)
  • \(1 \leq W \leq 10^5\)
  • \(1 \leq w_i \leq W\)
  • \(1 \leq v_i \leq 10^9\)

0-1 背包超级模板题。设 \(dp(i,j)\) 表示考虑 \(1 \sim i\) 个物品,背包装了 \(j\) 重量的物品的最大价值。每一次考虑选不选这个物品即可。

https://atcoder.jp/contests/dp/tasks/dp_d

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n;
ll a[N],b[N],c[N];
ll dp[N][3];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a[i]; 
		dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b[i]; 
		dp[i][2]=max(dp[i-1][0],dp[i-1][1])+c[i]; 
	}
	cout<<max({dp[n][0],dp[n][1],dp[n][2]});
	return 0;
}
/*
dp[i][0-2]表示第i天选择活动0-2的最大价值 
*/ 

E - Knapsack 2

\(N\) 个物品被编号为 \(1, 2, \ldots, N\)。对于 \(1 \leq i \leq N\),物品 \(i\) 的重量是 \(w _ i\),价值是 \(v _ i\)

太郎君决定从 \(N\) 个物品中选择一些放入背包中带回家。背包的容量为 \(W\),带回的物品的总重量不能超过 \(W\)

请计算太郎君能带回的物品的最大总价值。

  • \(1 \leq N \leq 100\)
  • \(1 \leq W \leq 10 ^ 9\)
  • \(1 \leq w _ i \leq W\)
  • \(1 \leq v _ i \leq 10 ^ 3\)

可以发现按照传统的套路枚举背包容量这个方法显然不行了。怎么办?

发现物品价值的值域比较小,所以可以想到设价值为状态。考虑 \(dp(i,j)\) 表示前 \(i\) 件物品中价值为 \(j\) 时的最小背包重量(显然装相同价值的物品时,背包重量越小越好,因为还得留出空间给后面的物品装进去)

状态转移就按照原来的背包改一下就好了。

https://atcoder.jp/contests/dp/submissions/70496512

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=107;
constexpr int M=1e5+7;
int n;
ll m,s,dp[N][M],w[N],c[N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>w[i]>>c[i],s+=c[i];
	for(int i=0;i<N;i++) for(int j=0;j<M;j++) dp[i][j]=1e18;
	dp[0][0]=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=s;j>=0;j--)
		{
			if(j>=c[i])
				dp[i][j]=min(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
			else
				dp[i][j]=dp[i-1][j];
		}
	}
	ll ans=-1e18;
	for(int i=1;i<=n;i++)
	{
		for(int j=s;j>=0;j--)
		{
			if(dp[i][j]<=m)
			{
				ans=max(ans,j*1ll);
				break;
			}
		}
	}
	cout<<ans;
	return 0;
}
/*
设dp[i][j]表示考虑前i件物品,物品总价值为j时的最小背包容量
第二维定义域:[1,1e5] 
*/

F - LCS

给定一个字符串 \(s\) 和一个字符串 \(t\) ,输出 \(s\)\(t\) 的最长公共子序列。

数据保证 \(s\)\(t\) 仅含英文小写字母,并且 \(s\)\(t\) 的长度小于等于3000。

DP 转移可以由 \(s/t\) 的前一位转移过来,很好写。主要是怎么记录转移路径:有一个很通用的套路(?)就是设 fa[i][j] 表示 dp[i][j] 这个状态是由谁转移过来的,然后直接从 fa[n][m] 开始往回找就可以了。

https://atcoder.jp/contests/dp/submissions/70496911

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=3007;
int dp[N][N];
pair<int,int> fa[N][N];
char s[N],t[N];
void print(int x,int y)
{
	if(x==0&&y==0) return;
	print(fa[x][y].first,fa[x][y].second);
	if(s[x]==t[y]) cout<<s[x];
}
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>(s+1)>>(t+1);
	int n=strlen(s+1),m=strlen(t+1);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(s[i]==t[j])
			{
				dp[i][j]=dp[i-1][j-1]+1;
				fa[i][j]={i-1,j-1};
			}
			else
			{
				if(dp[i-1][j]>dp[i][j-1])
				{
					dp[i][j]=dp[i-1][j];
					fa[i][j]={i-1,j};
				}
				else
				{
					dp[i][j]=dp[i][j-1];
					fa[i][j]={i,j-1};
				}
			}
		}
	}
	print(n,m);
	return 0;
}
/*
dp[i][j]表示s[1~i],t[1~j]的LCS 
*/

G - Longest Path

给定一个有 \(N\) 个顶点、\(M\) 条边的有向图 \(G\)。顶点编号为 \(1, 2, \ldots, N\)。对于每个 \(i\)\(1 \leq i \leq M\)),第 \(i\) 条有向边从顶点 \(x_i\) 指向顶点 \(y_i\)\(G\) 不包含有向环。

请你求出 \(G\) 中所有有向路径中最长的那条路径的长度。这里,有向路径的长度指的是该路径上包含的边的数量。

  • 所有输入均为整数。
  • \(2 \leq N \leq 10^5\)
  • \(1 \leq M \leq 10^5\)
  • \(1 \leq x_i, y_i \leq N\)
  • 所有的 \((x_i, y_i)\) 均互不相同。
  • \(G\) 不包含有向环。

一个 DAG 上的 DP 问题,考虑拓扑排序,设 \(dp(u)\) 表示走到 \(u\) 这个点时的最长路,然后对于 \(u\) 的出边 \(u \to v\) 就直接 \(dp(v)=dp(u)+1\) 就好了。

https://atcoder.jp/contests/dp/submissions/70501179

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
int n,m;
vector<int> g[N];
int dp[N];
int in[N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++)
	{
		cin>>u>>v;
		g[u].push_back(v);
		in[v]++;
	}
	queue<int> q;
	for(int i=1;i<=n;i++) if(in[i]==0) q.push(i);
	while(!q.empty())
	{
		int u=q.front(); q.pop();
		for(int v:g[u])
		{
			dp[v]=max(dp[v],dp[u]+1); --in[v];
			if(in[v]==0) q.push(v);
		}
	}
	cout<<*max_element(dp+1,dp+n+1);
	return 0;
}
/*
设dp[u]表示走到u时的最长路 
*/

H - Grid 1

有一个高 \(H\) 行、宽 \(W\) 列的网格。第 \(i\) 行第 \(j\) 列的格子用 \((i, j)\) 表示。

对于每个 \(i, j\)\(1 \leq i \leq H\)\(1 \leq j \leq W\)),格子 \((i, j)\) 的信息由字符 \(a_{i, j}\) 给出。如果 \(a_{i, j}\).,则格子 \((i, j)\) 是空格;如果 \(a_{i, j}\)#,则格子 \((i, j)\) 是墙。保证格子 \((1, 1)\)\((H, W)\) 都是空格。

太郎君从格子 \((1, 1)\) 出发,每次只能向右或向下移动到相邻的空格,目标是到达格子 \((H, W)\)

请问从 \((1, 1)\)\((H, W)\) 的路径有多少种?由于答案可能非常大,请输出答案对 \(10^9 + 7\) 取模的结果。

  • \(H\)\(W\) 是整数。
  • \(2 \leq H, W \leq 1000\)
  • \(a_{i, j}\) 只可能是 .#
  • \((1, 1)\)\((H, W)\) 都是空格。

状态很好设,\(dp(i,j)\) 表示走到 \((i,j)\) 时的路径条数,然后在 \((i,j)\) 这个点可以由 \((i-1,j),(i,j-1)\) 转移过来。很好写。

https://atcoder.jp/contests/dp/submissions/70495627

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1007;
constexpr ll mod=1e9+7;
int n,m;
char c[N][N];
ll dp[N][N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++) cin>>c[i][j];
	dp[1][1]=1; c[1][1]='#';
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			if(c[i][j]=='#') continue;
			dp[i][j]=(dp[i-1][j]+dp[i][j-1])%mod;
		}
	}
	cout<<dp[n][m];
	return 0;
}
/*
设dp[i][j]表示走到(i,j)的路径条数 
*/

I - Coins

\(N\) 是一个正的奇数。

\(N\) 枚硬币,每枚硬币上标有编号 \(1, 2, \ldots, N\)。对于每个 \(i\) (\(1 \leq i \leq N\)),掷硬币 \(i\) 时,正面朝上的概率是 \(p _ i\),反面朝上的概率是 \(1 - p _ i\)

太郎君把这 \(N\) 枚硬币全部投掷了一次。请计算正面朝上的硬币数多于反面朝上的硬币数的概率。

  • \(N\) 是奇数。
  • \(1 \leq N \leq 2999\)
  • \(p _ i\) 是实数,精确到小数点后两位。
  • \(0 < p _ i < 1\)

简单概率题(这居然有绿?)

首先可以考虑设 \(dp(i,j)\) 表示前 \(i\) 枚硬币,其中恰好有 \(j\) 枚正面朝上的概率,然后转移也很好写,两种可能:一种是这一枚正面朝上了(概率为 \(p_i\)),另一种是这一枚反面朝上了(概率为 \(1-p_i\)),转移就是:

\[dp(i,j)= \left\{ \begin{aligned} 1 &,& i=j=0 \\ dp(i-1,0)\times (1-p_i) &,& i \neq 0,j=0 \\ dp(i-1,j-1) \cdot p_i+dp(i-1,j) \cdot (1-p_i) &,& i \in [1,n],j \in [1,i] \end{aligned} \right. \]

https://atcoder.jp/contests/dp/submissions/70501942

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=3e3+10;
double p[N],dp[N][N];
int n;
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cout.flags(ios::fixed);
	cout.precision(10);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>p[i];
	dp[0][0]=1;
	for(int i=1;i<=n;i++) dp[i][0]=dp[i-1][0]*(1-p[i]);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=i;j++)
		{
			   //      这一枚朝上           这一枚不朝上 
			dp[i][j]=dp[i-1][j-1]*p[i] + dp[i-1][j]*(1-p[i]);
		}
	}
	double ans=0;
	for(int x=n;x>0&&x>(n-x);x--)
	{
	//	cerr<<x<<' '<<dp[n][x]<<endl;
		ans+=dp[n][x];
	}
	cout<<ans;
	return 0;
}
/*
dp[i][j]表示1~i枚硬币有j枚正面朝上的概率 
*/

J - Sushi

\(N\) 个盘子。每个盘子编号为 \(1, 2, \ldots, N\)。最初,对于每个 \(i\)\(1 \leq i \leq N\)),第 \(i\) 个盘子上有 \(a_i\)\(1 \leq a_i \leq 3\))个寿司。

太郎君会不断重复以下操作,直到所有寿司都被吃完:

  • 掷一个等概率出现 \(1, 2, \ldots, N\) 的骰子,掷出的点数为 \(i\)。如果第 \(i\) 个盘子上还有寿司,则吃掉一个寿司;如果没有寿司,则什么也不做。

请你求出吃完所有寿司所需操作次数的期望值。

  • 输入均为整数。
  • \(1 \leq N \leq 300\)
  • \(1 \leq a_i \leq 3\)

因为对期望不太熟练,所以有点不会写。

发现 \(n \le 300,a_i \le 3\),这个 \(a_i\) 比较小,所以可以考虑直接根据这个值域设状态。

实际上,因为骰子是等概率选取盘子的,所以我们只需要分别考虑 \(a_i=\{0,1,2,3\}\) 的盘子个数就好了,无需关心是第几个盘子。

\(dp(a,b,c)\) 表示剩余 \(1/2/3\) 个寿司的盘子有多少个,剩余 \(0\) 个的盘子可以直接由 \(n-a-b-c\) 计算得到。然后可以写出状态转移:

\[{\color{red}dp(a,b,c)}=\dfrac{(n-a-b-c)}{n} \cdot {\color{red}dp(a,b,c)} + \dfrac a n \cdot dp(a-1,b,c) \\ + \dfrac b n \cdot dp(a+1,b-1,c) + \dfrac c n \cdot dp(a,b+1,c-1) \]

可以发现这个转移成环了,要知道 \(dp(a,b,c)\),必须知道 \(dp(a,b,c)\) 就是左右脑互搏。所以可以考虑把这个 \(dp(a,b,c)\) 移项整理一下。

\[dp(a,b,c) = \frac{a \cdot dp(a-1,b,c) + b \cdot dp(a+1,b-1,c) + c \cdot dp(a,b+1,c-1) + n}{a + b + c} \]

注意到我们要知道 \(dp(a,b,c)\),必须知道 \(dp(a-1,b,c),dp(a+1,b-1,c),dp(a,b+1,c-1)\) 的值,所以我们应该从 \(c\) 开始枚举,然后到 \(b,a\)

https://atcoder.jp/contests/dp/submissions/70505827

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=309;
int n,cnt[5];
double dp[N][N][N];
inline double dp_(int i,int j,int k)
{
	if(i<0||j<0||k<0) return 0;
	return dp[i][j][k];
}
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n;
	for(int i=1,a;i<=n;i++)
	{
		cin>>a;
		cnt[a]++;
	}
	
	for(int k=0;k<=n;k++)
	{
		for(int j=0;j<=n;j++)
		{
			for(int i=0;i<=n;i++)
			{
				if(i==0&&j==0&&k==0) continue; 
				dp[i][j][k]=(i*dp_(i-1,j,k)+j*dp_(i+1,j-1,k)+k*dp_(i,j+1,k-1)+n)/(i+j+k);
			}
		}
	}
	cout<<fixed<<setprecision(10)<<dp[cnt[1]][cnt[2]][cnt[3]];
	return 0;
}
/*
dp[a][b][c]表示有1,2,3个寿司的盘子有a,b,c个时的期望步数
状态转移:
dp[a][b][c]=(n-a-b-c)/n*dp[a][b][c] + a/n*dp[a-1][b][c] + b/n*dp[a+1][b-1][c] + c/n*dp[a][b+1][c-1]
dp[a][b][c]=(a*dp[a-1][b][c]+b*dp[a+1][b-1][c]+c*dp[a][b+1][c-1]+n)/(a+b+c)
*/
posted @ 2025-10-28 13:19  wwwidk1234  阅读(24)  评论(0)    收藏  举报