#include <iostream>
#include <string>
using namespace std;
const int P = 998244353, N = 1e4 + 10, M = 20;
int n, m;
string s;
int dp[1 << M];
int solve() {
dp[0] = 1;
for (int i = 0; i < n; ++i) {
for (int j = (1 << (m - 1)) - 1; j >= 0; --j) {
int k = (j << 1) | (s[i] - '0');
if (j != 0 || s[i] == '1')
dp[k] = (dp[k] + dp[j]) % P;
}
}
int ans = 0;
for (int i = 0; i < (1 << m); ++i) {
ans = (ans + 1ll * i * dp[i]) % P;
}
return ans;
}
int solve2() {
int ans = 0;
for (int i = 0; i < (1 << n); ++i) {
int cnt = 0;
int num = 0;
for (int j = 0; j < n; ++j) {
if (i & (1 << j)) {
num = num * 2 + (s[j] - '0');
cnt++;
}
}
if (cnt <= m) (ans += num) %= P;
}
return ans;
}
int
(本题为阅读程序 / 完善程序,完整题目与测试数据见平台)这题核心是状态压缩DP:solve用dp[j]记录后缀模式j的子序列数,转移时把当前位拼到j后面;solve2是n≤20时的暴力枚举。两函数本质都是求长度≤m的子序列对应二进制数之和,区别在于实现方式与时间复杂度。
学生常忽略solve2只在n≤20时执行,以为两行输出永远相同;误把dp数组下标当成子序列长度而非后缀模式;在计算总和时忘记乘上数值i(直接加dp[i]会少算权重);模运算时先乘后加顺序搞错导致溢出或结果偏小。
这是一道典型的状态压缩动态规划题,同时结合了子序列计数与模运算。题目给出的两段代码其实是在用两种不同思路计算同一个东西:字符串s中所有长度不超过m的子序列,把它们当成二进制数,求这些数的总和(结果对998244353取模)。
状态压缩DP就是把集合用二进制位表示。这里dp[j]里的j是一个m位二进制数,第k位为1表示这个子序列末尾k位是1。比如m=3,j=5(二进制101)就代表末尾模式是“101”。
子序列是从原串按顺序挑出来的字符,可以不连续。比如s="10010",挑第1、3、5位得到"100",挑第2、4位得到"00",这些都是合法子序列。
为什么用状态压缩?因为m很小(≤20),2^m最多约100万,完全可以开数组存每个后缀模式出现的次数。
1. 两行输出是否总一样?很多学生看到代码里if(n≤20)就以为solve2只会在n小的时候跑,其实题目描述里说“完成下面的判断题和单选题”,隐含的意思是两行输出在n≤20时应该一致,否则就错了。命题人故意用这个条件来考你对算法复杂度的理解。 2. dp[j]到底存什么?容易误以为是“长度为j的子序列个数”,其实是“后缀模式为j的子序列个数”。 3. 转移时j=0要不要处理?代码里写了if (j != 0 || s[i] == '1'),这是为了避免把空后缀当成有效状态去转移,但初学者容易忽略这个细节。 4. 最终答案怎么算?是Σ i * dp[i],不是Σ dp[i]。很多同学会漏掉乘i,导致答案偏小。
拿到题,先读代码,发现solve()里有两层循环:外层遍历s的每个字符,内层遍历所有可能的后缀模式j。转移时把当前字符s[i]拼到j后面得到新模式k,然后dp[k] += dp[j]。这说明dp[j]就是“以j为后缀的子序列个数”。
再看solve2(),它直接枚举所有2^n个子集,对每个子集算出选出的字符组成的二进制数num,如果子集大小cnt≤m就累加到ans。这明显是暴力,复杂度O(2^n)。因为n≤20才跑,所以命题人用这个来考你对两种算法适用范围的把握。
假设输入:n=11, m=2, s="10000000001"
solve()的推导:
solve2()的推导:
所以两行输出分别是32和23,判断题2正确。
ans = (ans + dp[i]) % P而不是ans = (ans + 1ll * i * dp[i]) % P,答案会偏小。根据题库数据,动态规划共考过120题,覆盖9个级别,其中2024年考了24题,2025年25题,2026年30题,呈现逐年上升趋势。递推考了79题,2024年23题,2025年24题,同样在增加。状态压缩DP只有14题,但2024年考了1题,2026年考了7题,说明这类题在高端竞赛中越来越受青睐。计数DP仅1题,恰好是2024年的这道题,可见它是该类别的代表作。
从数据看,状态压缩DP在2026年突然考了7题,明显是命题组在加大这类题的权重。未来GESP八级、CSP提高级、NOIP提高组初赛都可能继续出类似题目,但m的范围大概率仍控制在10~20之间,以保证2^m可接受。同时,可能会把“两算法对比”作为新题型,比如问“当n>m时,solve()和solve2()的返回值差的最大可能值”,这正是选择题第5问的延伸。因此,备考时要重点掌握: 1. 如何定义dp状态(后缀模式); 2. 转移方程的推导(拼接当前位); 3. 答案的加权求和(Σ i·dp[i]); 4. 暴力解法的复杂度界限(O(2^n)); 5. 何时两算法等价(n≤m时,solve2只枚举长度≤m的子集,而solve的dp天然只统计长度≤m的后缀,故等价)。
暂无足够依据预测具体年份的考题形式,但上述方向基本可以锁定。
这道题用状态压缩DP巧妙地把子序列计数和二进制数值求和打包在一起,同时用暴力枚举作为对比,考察你对算法复杂度、状态定义和模运算的综合掌握。记住:dp[j]是后缀模式,转移是拼接,答案是加权求和,n>20时solve2不跑。把这些点吃透,这类题就稳了。