2023 CSP-S 复赛真题及答案解析(完整版)|四题全解
本文分两部分:前半部分为 2023 CSP-S 第二轮(复赛)完整真题,后半部分为四道题的详细解析与满分 C++ 代码。2023 年 S 组整体难度回落:T1 暴力枚举、T3 纯模拟送分,T2 栈 + DP 是"简单模型 + 巧妙递推",T4 二分 + 树上贪心是竞赛常规武器。建议先通读真题、独立尝试,再对照解析查漏补缺。
获取 2023CSP-S真题及解析.pdf
请关注状元编程公众号,回复 20261012csp-s
第一部分:真题原文
考试说明
考试:CSP-S 2023 第二轮认证(提高级) 时间:2023 年 10 月 21 日 四道题:密码锁(Lock)、消消乐(Game)、结构体(Struct)、种树(Tree)
T1 密码锁(Lock)
【题目描述】
小 Y 有一把五个拨圈的密码锁。每个拨圈上是从 0 到 9 的数字,且是循环的(9 拨动一个位置后变成 0 或 8)。
小 Y 采用的锁车方式是:从正确密码开始,随机转动密码锁仅一次;每次都是以某个幅度仅转动一个拨圈,或者同时转动两个相邻的拨圈(两个拨圈转动的幅度相同)。例如可从 00115 转成 11115,但不会转成 12115。
小 Y 记下了自己锁车后密码锁的 n 个状态(都不是正确密码)。问有多少种可能的正确密码,使得每个正确密码都能按上述锁车方式产生这 n 个状态。
【输入格式】 第一行一个正整数 n(状态数);接下来 n 行,每行五个整数表示一个状态。
【输出格式】 输出一行一个整数,表示能对应这 n 个状态的正确密码个数。
【样例】
输入:10 0 1 1 5输出:81【数据范围】 1 ≤ n ≤ 8。特殊性质 A:所有正确密码都可通过仅转动一个拨圈得到这 n 个状态。
T2 消消乐(Game)
【题目描述】
小 L 在玩一个一维的消消乐,一次只能消除两个相邻的元素。给定一个长度为 n 且仅由小写字母构成的字符串。称一个字符串可消除,当且仅当可以对其进行若干次操作(每次删除两个相邻相同字符,操作后剩余字符串拼接),使之成为空字符串。
问这个字符串的所有非空连续子串中,有多少个是可消除的。
【输入格式】 第一行一个正整数 n;第二行一个长度为 n、仅含小写字母的字符串。
【输出格式】 输出一行一个整数,表示答案。
【样例】
输入:8accabccb输出:5【数据范围】 1 ≤ n ≤ 2×10⁶。特殊性质 A:字符独立等概率选择;B:仅由 a、b 构成。
T3 结构体(Struct)
【题目描述】
需要模拟一种类似 C++ 的结构体定义方式,并计算内存占用。基本类型共 4 种:byte、short、int、long,分别占 1、2、4、8 字节。
定义结构体类型时给出类型名和成员(每个成员按顺序给出类型和名称,类型可为基本类型或已定义的结构体类型)。定义类型时不占内存。
定义元素时,元素按以下规则占内存:成员按定义顺序排布,地址需满足对齐规则——任何类型的大小和起始地址都应对齐到该类型对齐要求的整数倍。基本类型的对齐要求等于其大小;结构体的对齐要求等于其成员对齐要求的最大值。
需处理 n 次操作,共四种:
定义结构体类型:给定类型名 s、成员数 k,以及每个成员的类型和名称,输出该类型的大小和对齐要求。 定义元素:给定类型 t 和名称 n,元素从地址 0 起依次排布并满足对齐,输出新元素的起始地址。 访问元素:给定形如 a.b.c的路径,输出最内层元素的起始地址。访问内存地址:给定非负整数 addr,判断是否存在基本类型元素占据该地址,若存在按操作 3 格式输出,否则输出 ERR。
【输入格式】 第 1 行一个正整数 n(操作数);接下来每行第一个正整数 op 表示操作类型,随后按类型读入参数。
【输出格式】 输出 n 行,每行为对应操作的输出结果。
【样例】
输入:51 a 2short aaint ab1 b 2a balong bb2 b x3 x.ba.ab4 10输出:8 416 804x.bb【数据范围】 1 ≤ n ≤ 100,1 ≤ k ≤ 100,0 ≤ addr ≤ 10¹⁸。类型名、成员名、元素名均由不超过 10 个小写字母组成,且不与基本类型重名。保证结构体大小及最高内存地址不超过 10¹⁸。
对齐规则形式化:设结构体有 k 个成员,大小 s₁..sₖ,对齐 a₁..aₖ,则结构体对齐 a = max{aᵢ};偏移 o₁=0,oᵢ = 满足 oᵢ₋₁+sᵢ₋₁ ≤ oᵢ 且 aᵢ | oᵢ 的最小值;大小 s = 满足 oₖ+sₖ ≤ s 且 a | s 的最小值。
T4 种树(Tree)
【题目描述】
你是一个森林养护员。森林有 n 片地块,1 号地块连接入口,共 n−1 条道路使所有地块连通。最开始每片地块都没有树。
目标:在每片地块种一棵树,使 i 号地块的树高度长到不低于 aᵢ 米。
每天你可以选择一个未种树且与某个已种树地块直接邻接的地块,种一棵高度 0 米的树;第 1 天只能在 1 号地种树。第 x 天(从任务第一天算起),i 号地块的树长高 max(bᵢ + x×cᵢ, 1) 米。
求最少需要多少天完成任务。
【输入格式】 第一行一个正整数 n;接下来 n 行每行三个整数 aᵢ, bᵢ, cᵢ;接下来 n−1 行每行两个正整数 uᵢ, vᵢ(道路)。
【输出格式】 输出一行一个正整数,表示最少天数。
【样例】
输入:412 1 12 4 -110 3 07 10 -21 21 33 4输出:5【数据范围】 1 ≤ n ≤ 10⁵,1 ≤ aᵢ ≤ 10¹⁸,1 ≤ bᵢ ≤ 10⁹,0 ≤ |cᵢ| ≤ 10⁹。特殊性质 A:cᵢ=0;B:uᵢ=i, vᵢ=i+1;C:度数不超过 2;D:uᵢ=1。
第二部分:题目解析
T1 密码锁(Lock)|暴力枚举
考点:暴力枚举 / 逆向思维 难度:普及−,2023 年 T1 送分
思路:逆向枚举全部 10⁵ 个候选密码 x;对每个候选,生成 81 种操作(5 位 × 9 种幅度 + 4 对相邻 × 9 种幅度)得到结果集合 R(x)。x 合法 ⟺ 每个给定状态都 ∈ R(x)。
易错点:① 拨动幅度 1~9(不能为 0);② "相邻两位同时拨"必须相邻且幅度相同;③ 结果用整型比较(五位数压缩为 0..99999 整数)。
#include<bits/stdc++.h>usingnamespace std;int n;int state[10][5];intmain(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n;for (int i = 0; i < n; i++)for (int j = 0; j < 5; j++) cin >> state[i][j];longlong ans = 0;for (int x = 0; x < 100000; x++) { // 枚举候选正确密码bool okAll = true;for (int s = 0; s < n && okAll; s++) {bool okOne = false;for (int p = 0; p < 5 && !okOne; p++) // 转一个拨圈for (int w = 1; w <= 9; w++) {int cur[5];for (int j = 0; j < 5; j++) cur[j] = x / (int)pow(10, 4 - j) % 10; cur[p] = (cur[p] + w) % 10;int v = 0;for (int j = 0; j < 5; j++) v = v * 10 + cur[j];if (v == state[s][0]*10000 + state[s][1]*1000 + state[s][2]*100 + state[s][3]*10 + state[s][4]) { okOne = true; break; } }for (int p = 0; p < 4 && !okOne; p++) // 转相邻两个拨圈for (int w = 1; w <= 9; w++) {int cur[5];for (int j = 0; j < 5; j++) cur[j] = x / (int)pow(10, 4 - j) % 10; cur[p] = (cur[p] + w) % 10; cur[p+1] = (cur[p+1] + w) % 10;int v = 0;for (int j = 0; j < 5; j++) v = v * 10 + cur[j];if (v == state[s][0]*10000 + state[s][1]*1000 + state[s][2]*100 + state[s][3]*10 + state[s][4]) { okOne = true; break; } }if (!okOne) okAll = false; }if (okAll) ans++; } cout << ans << '\n';return0;}T2 消消乐(Game)|括号匹配栈 + DP
考点:栈 / 括号匹配模型 / 动态规划 难度:普及+/提高−,思维量适中
思路:可消除串 ⟺ 括号匹配串(每个字符与一个相同字符配对,配对区间嵌套或并列,无交叉)。用栈模拟从左到右消除:处理位置 i 时若与栈顶字符相同则配对弹出。设配对左端为 t(弹栈时的栈顶),则以 i 结尾的可消除子串数 f[i] = f[t−1] + 1,答案 = Σf[i]。
易错点:① 转移用 f[t−1](t 是弹栈栈顶);② 答案可达 O(n²)(如全相同字符),用 long long;③ 下标统一。
#include<bits/stdc++.h>usingnamespace std;typedeflonglong ll;constint MAXN = 2000005;int n;char s[MAXN];ll f[MAXN]; // f[i] = 以 i 结尾的可消除子串个数int stk[MAXN]; // 模拟消除栈(存位置)intmain(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> (s + 1); ll ans = 0;int top = 0;for (int i = 1; i <= n; i++) {if (top > 0 && s[stk[top]] == s[i]) {int t = stk[top--]; // t 与 i 配对 f[i] = f[t - 1] + 1; // "t..i" 本身 + 可消前缀的延长 } else { stk[++top] = i; f[i] = 0; } ans += f[i]; } cout << ans << '\n';return0;}T3 结构体(Struct)|对齐模拟
考点:模拟 / 结构体对齐 / 地址计算 难度:普及+/提高−,实现细致即可满分
思路:
类型信息:大小 size、对齐 align(= 成员对齐最大值),成员偏移按成员类型对齐规则 round_up 排布,size 最后 round_up 到 align。 变量声明:起始地址 = 当前空闲地址 round_up 到类型 align,cur 地址 += size。 访问 a.b.c:绝对地址 = 变量基址 + 逐层成员偏移之和。 反查地址(op=4):收集所有基本类型叶子的区间,逐一比对。
易错点:① 先按成员对齐算偏移,再算整体 size 与 align;② 嵌套结构体用其自身 align;③ 地址可达 2⁶³ 量级,用 long long。
#include<bits/stdc++.h>usingnamespace std;typedeflonglong ll;structType { ll size = 0, align = 0; vector<tuple<string,string,ll>> mem; // (成员名, 类型名, 偏移)};structVar { string type; ll start; };map<string, Type> typeMap;map<string, Var> varMap;vector<pair<string,ll>> basicRange; // 基本类型叶子 (路径, 起始地址)ll curAddr = 0;ll roundUp(ll x, ll a){ return a ? (x + a - 1) / a * a : x; }voiddefineType(string name, vector<pair<string,string>>& members){ Type t; t.size = 0; t.align = 1;for (auto &[tname, mname] : members) { Type &mt = typeMap[tname]; t.align = max(t.align, mt.align); ll off = roundUp(t.size, mt.align); t.mem.push_back({mname, tname, off}); t.size = off + mt.size; } t.size = roundUp(t.size, t.align); typeMap[name] = t; cout << t.size << ' ' << t.align << '\n';}ll accessOff(string path, string& leafType){ string varName = path.substr(0, path.find('.')); Var &v = varMap[varName]; string typeName = v.type; ll off = 0; string rest = path; rest.erase(0, rest.find('.') == string::npos ? rest.size() : rest.find('.') + 1);while (!rest.empty()) { string seg = rest.substr(0, rest.find('.')); Type &t = typeMap[typeName];for (auto &[mn, tn, mo] : t.mem)if (mn == seg) { off += mo; typeName = tn; break; }if (rest.find('.') == string::npos) rest = "";else rest.erase(0, rest.find('.') + 1); } leafType = typeName;return off;}intmain(){ ios::sync_with_stdio(false); cin.tie(nullptr); typeMap["byte"] = {1, 1, {}}; typeMap["short"] = {2, 2, {}}; typeMap["int"] = {4, 4, {}}; typeMap["long"] = {8, 8, {}};int n; cin >> n;while (n--) {int op; cin >> op;if (op == 1) { string s; int k; cin >> s >> k; vector<pair<string,string>> members(k);for (int i = 0; i < k; i++) cin >> members[i].first >> members[i].second;defineType(s, members); } elseif (op == 2) { string t, name; cin >> t >> name; ll start = roundUp(curAddr, typeMap[t].align); varMap[name] = {t, start}; curAddr = start + typeMap[t].size; cout << start << '\n';if (typeMap[t].mem.empty()) basicRange.push_back({name, start}); } elseif (op == 3) { string path; cin >> path; string leafType; string varName = path.substr(0, path.find('.')); ll off = accessOff(path, leafType); ll start = varMap[varName].start + off; cout << start << '\n';if (typeMap[leafType].mem.empty()) basicRange.push_back({path, start}); } else { ll addr; cin >> addr; string res = "ERR";for (auto &[path, st] : basicRange) { string leafType; ll off = accessOff(path, leafType); ll sz = typeMap[leafType].size;if (st <= addr && addr < st + sz) { res = path; break; } } cout << res << '\n'; } }return0;}T4 种树(Tree)|二分答案 + 树上贪心
考点:二分答案 / 树上贪心 / 优先队列 难度:提高,2023 年压轴题但做法经典
思路:
二分总天数 D。 对每棵树计算"最晚种植日" deadlineᵢ(在 D 天内能长到 aᵢ 的最晚日期),由 b、c 的符号分段讨论(c≥0 单调不减,c<0 单调递减需钳制 max(·,1))。 检查:每天必须种一棵"父亲已种"且 deadline 最紧迫的节点——从根开始,用优先队列按 deadline 贪心取;若任意时刻取不出或种晚于 deadline → D 不可行。
易错点:① cᵢ=0 时高度恒定 bᵢ;② cᵢ<0 时 deadline 要分段;③ "第 x 天"从任务首日计;④ 二分上界用 1e9 足够。
#include<bits/stdc++.h>usingnamespace std;typedeflonglong ll;constint MAXN = 100005;int n;ll a[MAXN], b[MAXN], c[MAXN];vector<int> g[MAXN];ll deadlineArr[MAXN];ll growth(ll day, ll B, ll C){if (day <= 0) return0;if (C >= 0) {return day * B + C * day * (day + 1) / 2; } else { ll xstar = (1 - B) / C;if (xstar < 0) xstar = 0; ll t = min(day, xstar);return t * B + C * t * (t + 1) / 2 + (day - t) * 1; }}ll calcDeadline(ll D, ll A, ll B, ll C){if (growth(D, B, C) < A) return-1; ll lo = 1, hi = D;while (lo < hi) { ll mid = (lo + hi + 1) / 2;if (growth(D, B, C) - growth(mid - 1, B, C) >= A) lo = mid;else hi = mid - 1; }return lo;}boolcheck(ll D){for (int i = 1; i <= n; i++) { deadlineArr[i] = calcDeadline(D, a[i], b[i], c[i]);if (deadlineArr[i] == -1) returnfalse; } priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;vector<int> vis(n + 1, 0); pq.push({deadlineArr[1], 1});for (int day = 1; day <= n; day++) {if (pq.empty()) returnfalse;auto [dl, u] = pq.top(); pq.pop();if (day > dl) returnfalse; vis[u] = 1;for (int v : g[u]) if (!vis[v]) pq.push({deadlineArr[v], v}); }returntrue;}intmain(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n;for (int i = 1; i <= n; i++) cin >> a[i] >> b[i] >> c[i];for (int i = 1; i < n; i++) {int u, v; cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } ll lo = n, hi = 1000000000LL + 5;while (lo < hi) { ll mid = (lo + hi) / 2;if (check(mid)) hi = mid;else lo = mid + 1; } cout << lo << '\n';return0;}四题考点总结
💡 年份点评:2023 年整体难度回落——T1 暴力枚举、T3 纯模拟送分到位;T2 栈+DP 是"简单模型 + 巧妙递推";T4 二分+贪心是竞赛常规武器。T2 把字符串消消乐化归为括号匹配是全场思维亮点;T4 的"deadline 最紧迫优先"是"可行性判定"类题目的通用范式,值得专门总结。
本文真题基于 2023 CSP-S 第二轮认证官方试卷整理,答案与解析供学习参考,最终以 CCF 官方发布为准。祝各位同学复赛顺利!