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\) 时的最小花费,决策是从前一个还是前两个台阶跳过来。
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\)),转移就是:
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\) 计算得到。然后可以写出状态转移:
可以发现这个转移成环了,要知道 \(dp(a,b,c)\),必须知道 \(dp(a,b,c)\) 就是左右脑互搏。所以可以考虑把这个 \(dp(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)
*/

浙公网安备 33010602011771号