Atcoder Educational DP Contest 题解
update on 2025.11.10 11:25 完成 F,G,I,J.
update on 2025.11.10 17:30 完成 K,L,M,N,O.
update on 2025.11.10 21:45 完成 P,Q,R.
update on 2025.11.14 22:07 完成 S.
:::info[注]{open} 过于简单的 A,B,C,D,E,H 不再赘述。
目前尚未完成 T,U,V,W,X,Y,Z 共
F. LCS
先求出 LCS,然后从后往前逆推一遍,具体地,从
- 若
s[x] = t[y] ,说明这一位匹配,记录答案(先存起来,因为顺序是反着的),并推到x = y 。- 解释:对应转移
dp[x][y] = dp[x-1][y-1] + 1 。
- 解释:对应转移
- 否则,这一位不匹配,找到
dp[x][y-1],dp[x-1][y] 中较大的转移过去,具体地:- 若
dp[x][y-1] < dp[x-1][y] ,x \to x-1 ; - 否则,
y \to y-1 。 - 解释:对应转移
dp[x][y] = \max(dp[x-1][y],dp[x][y-1]) 。
- 若
G. Longest Path
DAG 最长路,拓扑排序的过程中记录
I. Coins
概率 DP,设
初值:
转移:
-
-
边界转移:
-
时间复杂度
不过我们还可以优化一下,发现我们关注的是
设
转移类似,不过注意一个问题。
假设我们现在已经抛了
原因很简单,分讨一下:
- 若
i 为奇数,那么正、反可能分别为:奇、偶;偶、奇。而 奇 - 偶 = 偶 - 奇 = 奇。 - 若
i 为偶数,那么正、反可能分别为:偶、偶;奇、奇。而 奇 - 奇 = 偶 - 偶 = 偶。
那么更新时注意下奇偶性即可,优化表现在空间上,空间复杂度
这种只关注两维差值的 DP 的这种 Trick 很常见,比如 [CSP-S 2019] Emiya 家今天的饭 中也有应用。
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
int n;
double ans,p,dp[6005];
int main()
{
scanf("%d",&n);
dp[n] = 1;
for (int i=1;i<=n;i++)
{
scanf("%lf",&p);
for (int j=n-i;j<=n+i;j++)
{
if (i % 2 == (j+n) % 2) dp[j] = dp[j-1] * p + dp[j+1] * (1-p);
}
}
for (int i=1;i<=n;i+=2) ans += dp[n+i];
printf("%.10lf",ans);
return 0;
}
:::
J. Sushi
蛮好的一道 期望 DP 入门题。
很容易发现
换而言之,1 3 2 和 2 3 1 的答案是相同的,因为每次选中各个盘子的概率相同。
因此,我们考虑设
这个状态定义空间是
接下来看转移:
-
如果取了有
3 个寿司的盘子;- 概率为
\frac{i}{n} 。 - 转移到
dp[i-1][j+1][k] (因为有个3 的盘子变成了2 ),并且期望+1 。 - 即:
dp[i][j][k] \gets (dp[i-1][j+1][k]+1) \times \frac{i}{n} .
- 概率为
-
如果取了有
2 个寿司的盘子;-
概率为
\frac{j}{n} 。 -
转移到
dp[i][j-1][k+1] (因为有个2 的盘子变成了1 ),并且期望+1 。 -
即:
dp[i][j][k] \gets (dp[i][j-1][k+1]+1) \times \frac{j}{n} .
-
-
如果取了有
1 个寿司的盘子;- 概率为
\frac{k}{n} 。 - 转移到
dp[i][j][k-1] (因为有个1 的盘子变成了0 ),并且期望+1 。 - 即:
dp[i][j][k] \gets (dp[i][j][k-1]+1) \times \frac{k}{n} .
- 概率为
-
如果取了有
0 个寿司的盘子;- 概率为
\frac{n-i-j-k}{n} 。 - 转移到
dp[i][j][k] (因为有个0 的盘子变成了0 ,本质不变),并且期望+1 。 - 即:
dp[i][j][k] \gets (dp[i][j][k]+1) \times \frac{n-i-j-k}{n} .
- 概率为
整理得到转移式子:
发现存在「自己转移到自己」的情况,不满足 DP「无后效性」的要求。
怎么办?移项!
先把
再移项:
合并同类项,两边同时乘上
把左边的系数
根据上面的转移式转移即可,注意几点:
-
初始化
dp[0][0][0] = 0 ,不要把i+j+k = 0 (即i,j,k=0 时再转移一下,否则除0 错误)。除
0 错误在double类型中 不会 RE,会变成\text{inf} 。 -
由于转移存在
-1 ,因此注意i = 0,j=0,k=0 三种情况。 -
取值范围,
0 \le i \le \sum_p [a[p]=3],0 \le j \le \sum_p [a[p]\ge 2],0 \le k \le \sum_p [a[p]\ge 1] 。注意后面两者,因为
3 会变成2 ,2 会变成1 。
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
int n,x,c[4];
double dp[305][305][305];
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%d",&x);
c[x]++;
}
for (int i=0;i<=c[3];i++)
{
for (int j=0;j<=c[2]+c[3];j++)
{
for (int k=0;k<=c[1]+c[2]+c[3];k++)
{
if (i+j+k == 0) continue;
if (i) dp[i][j][k] += (dp[i-1][j+1][k]+1) * (i*1.0/(i+j+k));
if (j) dp[i][j][k] += (dp[i][j-1][k+1]+1) * (j*1.0/(i+j+k));
if (k) dp[i][j][k] += (dp[i][j][k-1]+1) * (k*1.0/(i+j+k));
dp[i][j][k] += (n-(i+j+k))*1.0/(i+j+k);
}
}
}
printf("%.10lf",dp[c[3]][c[2]][c[1]]);
return 0;
}
:::
K. Stones
坑,开始一眼完全背包,设
:::warning[代码]{open}
#include <bits/stdc++.h>
using namespace std;
int n,k,x;
bool dp[100005];
int main()
{
scanf("%d%d",&n,&k);
for (int i=1;i<=n;i++)
{
scanf("%d",&x);
for (int j=x;j<=k;j++)
{
dp[j] = dp[j] | (dp[j-x]^1);
}
}
if (dp[k]) puts("First");
else puts("Second");
return 0;
}
:::
过了所有样例,But Wrong Answer.
为什么?发现 Hack:
【输入 #1】
2 3
1 2
【输出 #1】
Second
因此,错误的原因就在 dp[j] = dp[j] | (dp[j-x]^1); 这行。因为
在这个 Hack 中的表现就是,初始
异或的原因是先后手互换。
所以内外层循环换个位置即可,时间复杂度
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
int n,k,a[105];
bool dp[100005];
int main()
{
scanf("%d%d",&n,&k);
for (int i=1;i<=n;i++) scanf("%d",&a[i]);
for (int i=1;i<=k;i++)
{
for (int j=1;j<=n;j++)
{
if (i-a[j] < 0 || dp[i-a[j]]) continue;
dp[i] = 1;
break;
}
}
if (dp[k]) puts("First");
else puts("Second");
return 0;
}
:::
L. Deque
可以先看这道题 iai 2024年3月乙组 - 交替取数,没有太多的分类讨论,但思路相似。
发现每次取完之后剩下的都是一个区间,考虑 区间 DP。
设
具体地,若
那么根据操作方分类讨论:
- 若此时轮到 A(所取的数作为
X ,要求X-Y 尽可能大):- 如果取
a[l] ,那么答案变为a[l]+dp[l+1][r] 。 - 如果取
a[r] ,那么答案变为a[r] + dp[l][r-1] 。 - 由此,转移
dp[l][r] = \max(a[l]+dp[l+1][r],a[r]+dp[l][r-1]) 。
- 如果取
- 若此时轮到 B(所取的数作为
Y ,要求X-Y 尽可能小):- 如果取
a[l] ,那么答案变为dp[l+1][r]-a[l] 。 - 如果取
a[r] ,那么答案变为dp[l][r-1]-a[r] 。 - 由此,转移
dp[l][r] = \max(dp[l+1][r]-a[l],dp[l][r-1]-a[r]) 。
- 如果取
最后看初值,对 A、B 取最后一个分类讨论。
- 若 A 取(作
X ):dp[i][i] = a[i] 。 - 若 B 取(作
Y ):dp[i][i] = -a[i] 。
答案即为
时空复杂度
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
#define intt long long
int n;
intt a[3005],dp[3005][3005];
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
if (n % 2) dp[i][i] = a[i];
else dp[i][i] = -a[i];
}
for (int len=2;len<=n;len++)
{
for (int l=1;l+len-1<=n;l++)
{
int r = l+len-1;
if (len % 2 == n % 2)
{
dp[l][r] = max(a[l]+dp[l+1][r],a[r]+dp[l][r-1]);
}
else
{
dp[l][r] = min(dp[l+1][r]-a[l],dp[l][r-1]-a[r]);
}
}
}
printf("%lld",dp[1][n]);
return 0;
}
:::
M. Candies
显然的 DP:设
边界:
转移只需枚举给第
时间复杂度
发现这个形式是一段 DP 数组的求和,可以写成前缀和的形式。
具体地,记
又因为
这种优化方式被称为 前缀和优化 DP。
总时空复杂度均为
:::info[Trick]{open}
由于本题中允许分
因此
这样就可以很好的避免这个问题,查询时从”上界不变,下界
还可以进一步优化,发现
空间复杂度优化到
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
const int p = 1e9+7;
int n,k,x;
int sum[100005],dp[100005];
int mod(int x)
{
if (x < 0) x += p;
if (x >= p) x -= p;
return x;
}
int main()
{
scanf("%d%d",&n,&k);
dp[0] = 1;
for (int j=0;j<=k;j++) sum[j+1] = 1;
for (int i=1;i<=n;i++)
{
scanf("%d",&x);
for (int j=0;j<=k;j++)
{
dp[j] = mod(sum[j+1]-sum[j-min(j,x)]);
}
for (int j=0;j<=k;j++)
{
sum[j+1] = mod(sum[j]+dp[j]);
}
}
printf("%d",dp[k]);
return 0;
}
:::
N. Slimes
同 P1775. 石子合并(弱化版),区间 DP 枚举分界点即可。
O. Matching
我居然切蓝的 DP 了???
时间复杂度
:::info[时间复杂度解析]{open}
矩阵大小:
矩阵乘法:时间复杂度
快速幂:时间复杂度
实际上还有一个
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
#define intt long long
const int mod = 1e9+7;
int n;
intt sum,k;
struct martix
{
intt a[55][55];
}a,ans;
martix multi(martix x,martix y)
{
martix res;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
res.a[i][j] = 0;
}
}
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
for (int k=1;k<=n;k++)
{
res.a[i][j] = (res.a[i][j] + x.a[i][k] * y.a[k][j] % mod) % mod;
}
}
}
return res;
}
martix qpow(martix x,intt y)
{
martix res;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=n;j++)
{
res.a[i][j] = 0;
}
res.a[i][i] = 1;
}
while (y)
{
if (y & 1) res = multi(res,x);
x = multi(x,x);
y >>= 1;
}
return res;
}
int main()
{
scanf("%d%lld",&n,&k);
for (int i=1;i<=n;i++)
{
ans.a[1][i] = 1;
for (int j=1;j<=n;j++)
{
scanf("%lld",&a.a[i][j]);
}
}
ans = multi(ans,qpow(a,k));
for (int i=1;i<=n;i++)
{
sum = (sum + ans.a[1][i]) % mod;
}
printf("%lld",sum);
return 0;
}
:::
S. Digit Sum
数位 DP 模板题。
看到「十进制数位之和是
这样倍数转「取模为
那么我们考虑设
考虑转移,显然需要枚举
那么转移有:
稍微解释一下,这个转移是求和的形式,就是考虑填上这个数
初值是
接下来我们考虑数位 DP 中我认为细节较多且较为困难的部分: 统计答案。
一般数位 DP 统计答案的思路是,从高到低枚举数位,然后固定枚举过的位置,计算后面的方案数。
:::info[举例]
举个例子,我们现在要计算
- 枚举到最高位
2 ,这一位上如果放0 \sim 1 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
SXXXXXXX ,其中S 表示这一位可以放的值,即0 \sim 1 。
- 此时相当于拼成了
- 枚举到次高位
0 ,这一位上不管放什么,后面的都不能随便放,不统计答案。 - 枚举到下一位
2 ,这一位上如果放0 \sim 1 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
20SXXXXX ,其中S 表示这一位可以放的值,即0 \sim 1 。
- 此时相当于拼成了
- 枚举到下一位
5 ,这一位上如果放0 \sim 4 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
202SXXXX ,其中S 表示这一位可以放的值,即0 \sim 4 。
- 此时相当于拼成了
- 枚举到下一位
1 ,这一位上如果放0 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
2025SXXX ,其中S 表示这一位可以放的值,即0 。
- 此时相当于拼成了
- 枚举到下一位
1 ,这一位上如果放0 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
20251SXX ,其中S 表示这一位可以放的值,即0 。
- 此时相当于拼成了
- 枚举到下一位
1 ,这一位上如果放0 ,那么后面的可以随便放,统计答案。- 此时相当于拼成了
202511SX ,其中S 表示这一位可以放的值,即0 。
- 此时相当于拼成了
- 枚举到下一位
4 ,这一位上放0 \sim 4 都是可以的,统计答案。- 此时相当于拼成了
2025111S ,其中S 表示这一位可以放的值,即0\sim 4 。
- 此时相当于拼成了
:::
我们发现,对于非最后一位(记作第
对于最后一位,特别地记得加上最后一位放
这个统计可能比较抽象,可以结合代码看看:
:::info[统计答案的代码]{open}
for (int i=n;i>=1;i--)
{
for (int j=0;j<(int)(s[n-i+1]-'0')+(i==1);j++)
{
//这里的 s 是用 char 数组的形式存的 K,最高位对应的是 s[1],最低 s[n]。
ans = mod(ans + dp[i-1][((d-(x+j)%d)%d]);
//这里解释一下,x 是这一位(第 i 位)之前的 K 的数位之和,
//再算上这一位填的值 j,已经放的数位 mod d 的值是 (x+j)%d,
//但是要凑成总的数位之和 mod d = 0,因此剩下还需要补一个 (d-(x+j)%d)%d。
}
x = (x+(int)(s[n-i+1]-'0')) % d;
//这里就是统计下前面的数位之和,因为已经是固定的了。
}
:::
最后注意一下,这题求的计数是
总时间复杂度
:::success[代码]
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9+7;
char s[10005];
int x,d,n,dp[10005][105],ans;
int mod(int x)
{
if (x >= MOD) x -= MOD;
return x;
}
int dod(int x)
{
while (x < 0) x += d;
while (x >= d) x -= d;
return x;
}
int main()
{
scanf("%s",s+1);
n = strlen(s+1);
scanf("%d",&d);
dp[0][0] = 1;
for (int i=1;i<=n;i++)
{
for (int j=0;j<d;j++)
{
for (int k=0;k<=9;k++)
{
dp[i][dod(j+k)] = mod(dp[i][dod(j+k)]+dp[i-1][j]);
}
}
}
for (int i=n;i>=1;i--)
{
for (int j=0;j<(int)(s[n-i+1]-'0')+(i==1);j++)
{
ans = mod(ans + dp[i-1][dod(-x-j)]);
}
x = dod(x+(int)(s[n-i+1]-'0'));
}
printf("%d",mod(MOD+ans-1));
return 0;
}
:::
想明白了还是不难理解的。