题解:P11856 [CSP-J2022 山东] 吟诗
这道题给当时只学了两个月 C++ 的我偌大的震撼,时隔多年,满是回忆。
发现正着数太难想,反着想反而更加清晰。于是思路是先计算出所有不含妙手的诗的数量,再用总方案数 \(10^N\) 减去不含妙手的数量,得到答案。
我们用状态压缩 DP 来记录后缀和状态,定义:
- 令 \(dp[i][st]\) 表示前 \(i\) 个数字构成后缀和状态为 \(st\) 的方案数。
- 状态 \(st\) 用二进制表示:若第 \(k\) 位为 \(1\),则表示存在一个后缀和为 \(k+1\);这里只记录不超过 \(X+Y+Z\) 的后缀和。
对每个位置 \(i\) 和状态 \(st\) 以及枚举当前数字 \(d\)(\(1 \le d \le 10\)),更新后缀和状态为:
\[t = \left\{
\begin{array}{l}
t \mathrel{|}= (1 << (d-1)) \quad \text{(如果 $d \le X+Y+Z$)} \\
\text{对于 $st$ 中每个存在的后缀和 $s$(对应二进制第 $s-1$ 位为1),若 $s+d \le X+Y+Z$,则}
\quad t \mathrel{|}= (1 << (s+d-1))
\end{array}
\right.
\]
状态转移方程为:
\[dp[i+1][t] \mathrel{+}= dp[i][st]
\]
但如果新状态 \(t\) 中同时存在后缀和 \(Z\)、\(Y+Z\) 和 \(X+Y+Z\)(即对应二进制位均为 \(1\)),说明序列中已经出现妙手,此转移应舍去。
统计所有 \(dp[N][st]\) 得到不含妙手的序列数 \(cnt\) ,答案即为:
\[ans = 10^N - cnt \quad (\text{对 }998244353 \text{ 取模})
\]
时间复杂度 \(O(10 \times N \times 2^{X+Y+Z})\)。
#include<bits/stdc++.h>
using namespace std;
#define MAXN 50
#define MOD 998244353
#define ST 1 << 18
#define int long long
int dp[MAXN][ST];
signed main() {
int N, X, Y, Z;
cin >> N >> X >> Y >> Z;
int sum = X + Y + Z;
int lim = (1 << sum);
for (int i = 0; i < MAXN; i++) {
for (int j = 0; j < lim; j++) {
dp[i][j] = 0;
}
}
dp[0][0] = 1;
int tot = 1;
for (int i = 0; i < N; i++) {
tot = (tot * 10) % MOD;
}
for (int i = 0; i < N; i++) {
for (int st = 0; st < lim; st++) {
if(dp[i][st] == 0) continue;
for (int d = 1; d <= 10; d++) {
int t = 0;
if(d <= sum)
t |= (1 << (d - 1));
for (int bit = 0; bit < sum; bit++) {
if ((st >> bit) & 1) {
int cur = bit + 1;
int ns = cur + d;
if(ns <= sum) {
t |= (1 << (ns - 1));
}
}
}
if( (t & (1 << (Z - 1))) && (t & (1 << ((Y + Z) - 1))) && (t & (1 << (sum - 1))) ) continue;
dp[i+1][t] = (dp[i+1][t] + dp[i][st]) % MOD;
}
}
}
int cnt = 0;
for (int st = 0; st < lim; st++) {
cnt = (cnt + dp[N][st]) % MOD;
}
int ans = tot - cnt;
if(ans < 0) ans += MOD;
cout << ans % MOD << "\n";
return 0;
}

浙公网安备 33010602011771号