CSP-J · 2025第二轮真题
“会做”之后,还要把程序写稳。
两道基础题,练清读题、思路和边界。
《拼数》 · 《座位》|原题+过程图解+完整代码
看到2025年CSP-J复赛前两题,不少孩子会觉得:“数字排个序,座位算个位置,好像都不难。”
但从知道大致做法,到把程序写得完整、正确,中间还隔着几处很容易漏掉的细节。
这篇不先给模板。我们按原题题面 → 思路推导 → 完整代码 → 易错点,把两道题一步步拆开。建议先停在题面处,自己想一遍,再向下看解析。
01
PART
T1《拼数》
NUMBER · 从题面到实现
原题题面 · 先读题,再看解析
题目描述
小 R 正在学习字符串处理。小 X 给了小 R 一个字符串 s,其中 s 仅包含小写英文字母及数字,且包含至少一个1~9中的数字。小 X 希望小 R 使用 s 中的任意多个数字,按任意顺序拼成一个正整数。注意:小 R 可以选择 s 中相同的数字,但每个数字只能使用一次。例如,若 s 为1a01b,则小 R 可以同时选择第1、3、4个字符,分别为1、0、1,拼成正整数101或110;但小 R 不能拼成正整数111,因为 s 仅包含两个数字1。
小 R 想知道,在他所有能拼成的正整数中,最大的是多少。你需要帮助小 R 求出他能拼成的正整数的最大值。
输入格式
从文件 number.in 中读入数据。输入的第一行包含一个字符串 s,表示小 X 给小 R 的字符串。
输出格式
输出到文件 number.out 中。输出一行一个正整数,表示小 R 能拼成的正整数的最大值。
样例1
5
5
s 仅包含一个数字5,因此小 R 仅能拼成正整数5。
样例2
290es1q0
92100
s 包含数字2、9、0、1、0。可以证明,小 R 拼成的正整数的最大值为92100。
数据范围(精简)
字符串长度:1 ≤ |s| ≤ 10⁶。仅含小写英文字母和数字,保证至少有一个非零数字。
时限1秒,内存512 MiB。提交文件:number.cpp。
思路分析|先决定选哪些,再决定怎样排
“任意多个、任意顺序”看起来像需要尝试很多方案,但百万字符的范围不允许枚举所有选择和排列。我们要先利用正整数的比较规则,把问题化简。
没有前导零时,先比较位数;位数相同时,再从高位比较数字。由此可以确定方向:先保留尽可能多的数字,再让较大的数字尽量靠前。
数字只有0~9十种,因此实现时不用逐个比较排序,统计次数,再倒序输出即可。下面分别说明这几个判断为什么成立。
第一步:为什么数字都要选?
题面写的是“任意多个数字”,但允许少选,不代表少选更优。题目保证至少有一个非零数字,我们可以把它放在最高位;在没有前导零的情况下,位数更多的正整数更大。
所以,所有数字字符都应该保留,包括0。比如从 a3b0092 中选出的数字是 3、0、0、9、2,只用 9、3、2 得到932;把两个0也留下,可以得到93200,明显更大。
第二步:同样的位数,为什么从大到小?
两个正整数位数相同,就从左向右比较。第一个不同的位置,谁的数字更大,谁就更大。因此应优先把较大的数字放在前面,最终顺序为 9、3、2、0、0。

第三步:统计十种数字,按次数输出
建立 cnt[10],统计每个数字出现的次数。遇到数字字符 ch,用 ch - '0' 得到数组下标;字母直接跳过。
cnt[0] = 2;
cnt[2] = 1;
cnt[3] = 1;
cnt[9] = 1;
最后从9到0依次输出,每个数字输出它出现的次数。字符串可能长达100万字符,答案也可能有很多位,不能用 int 或 long long 保存整个数;使用字符串拼接或直接输出即可。
完整参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("number.in", "r", stdin);
freopen("number.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int cnt[10] = {}; // 各数字的出现次数
for (char ch : s) {
if (ch >= '0' && ch <= '9') {
cnt[ch - '0']++; // 转为数字
}
}
// 答案可能有百万位,不能用整数类型保存
string ans;
ans.reserve(s.size());
// 从大到小拼接,重复数字与0都保留
for (int d = 9; d >= 0; d--) {
ans.append(cnt[d], char('0' + d));
}
cout << ans << '\n';
return 0;
}
设输入长度为L,扫描和输出合计为O(L)时间。十个计数器本身只占常数空间;本实现保存输入字符串和答案,因此额外空间为O(L)。
容易丢分的三点:丢掉0、把重复数字去重、用整数变量累积答案。题目不是选出“不同的数字”,而是每个出现的位置都可以使用一次。
02
PART
T2《座位》
SEAT · 从排名到坐标
原题题面 · 先读题,再看解析
题目描述
CSP-J 2025第二轮正在进行。小 R 所在的考场共有 n × m 名考生,其中所有考生的 CSP-J 2025第一轮成绩互不相同。所有 n × m 名考生将按照 CSP-J 2025第一轮的成绩,由高到低蛇形分配座位,排列成 n 行 m 列。
具体地,设所有考生的成绩从高到低分别为 s₁ > s₂ > … > sₙₘ。成绩为 s₁ 的考生坐第1列第1行,成绩为 s₂ 的考生坐第1列第2行,……,成绩为 sₙ 的考生坐第1列第n行;成绩为 sₙ₊₁ 的考生坐第2列第n行,……,成绩为 s₂ₙ 的考生坐第2列第1行;成绩为 s₂ₙ₊₁ 的考生坐第3列第1行,以此类推。
例如,若 n=4、m=5,则所有20名考生按照成绩从高到低,根据下图中的箭头顺序蛇形分配座位。

给定小 R 所在考场座位的行数 n 与列数 m,以及所有考生第一轮的成绩 a₁、a₂、…、aₙₘ,其中 a₁ 为小 R 的成绩。你需要帮助小 R 求出,他的座位为第几列第几行。
输入格式
从文件 seat.in 中读入数据。第一行包含两个正整数 n、m,分别表示行数与列数。第二行包含 n × m 个正整数 a₁、a₂、…、aₙₘ,分别表示所有考生的成绩,其中 a₁ 为小 R 的成绩。
输出格式
输出到文件 seat.out 中。输出一行两个正整数 c、r,表示小 R 的座位为第c列第r行。
样例1
2 2
99 100 97 98
1 2
按成绩从高到低,100分坐第1列第1行,99分坐第1列第2行,98分坐第2列第2行,97分坐第2列第1行。小 R 得99分,因此坐第1列第2行。
样例2
2 2
98 99 100 97
2 2
座位分配顺序与样例1相同,小 R 得98分,因此坐第2列第2行。
样例3
3 3
94 95 96 97 98 99 100 93 92
3 1
数据范围(精简)
1 ≤ n、m ≤ 10,最多100名考生;1 ≤ aᵢ ≤ 100,所有成绩互不相同。
时限1秒,内存512 MiB。提交文件:seat.cpp。
思路分析|把成绩排序与座位定位分开
题目给出全部成绩,却只问小 R 一个人的位置。因此不必把整个考场的座位全部存下来,先确定他的成绩排第几,再将这个排名换成坐标即可。
成绩互不相同,比他高分的人数就能确定排名;每列坐n人,整除确定列号,取余确定列内位置,最后根据列号的奇偶决定从上还是从下数。
本题最多100人,排序后逐列模拟也能通过。这里选用统计排名+位置计算,重点练清编号、方向与边界,而不是追求复杂算法。
第一步:不必排序,先求自己的排名
座位只由成绩排名决定。成绩互不相同,因此排名=比自己分数高的人数+1。第一个读入的成绩就是小 R 的成绩,把它记为 target,继续读其他成绩并计数即可。
第二步:看清是按列走,不是按行走
假设考场有3行3列,把排名而不是分数填进格子,顺序如下。第一列向下,第二列向上,第三列再向下。

第三步:列号为什么要先减1?
把排名记为 k。每列容纳n人,但排名从1开始,所以先将它换成从0开始的编号 k - 1,再做整除和取余。
int col = (k - 1) / n + 1;
int offset = (k - 1) % n;
col 是从1开始的列号,offset 是从0开始的列内偏移。奇数列向下:行号为 offset + 1;偶数列向上:行号为 n - offset。这里的除法是整数除法。

每列3人时,第3名还在第1列,第4名才进入第2列。如果直接用 k / n + 1,就会把恰好坐满一列的最后一人提前放进下一列。
用原题样例3走一遍
小 R 的成绩是94分,比他高的有95至100共6人,因此排名为7。考场有3行,计算过程如下。
完整参考代码
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("seat.in", "r", stdin);
freopen("seat.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
int target; // 第一个成绩属于小R
cin >> target;
int rank = 1; // 排名=比自己高分的人数+1
for (int i = 1; i < n * m; i++) {
int score;
cin >> score;
if (score > target) rank++;
}
// 排名先减1,避免整列边界算错
int col = (rank - 1) / n + 1;
int offset = (rank - 1) % n;
int row;
// 奇数列向下,偶数列向上
if (col % 2 == 1) {
row = offset + 1;
} else {
row = n - offset;
}
// 输出顺序:列、行
cout << col << ' ' << row << '\n';
return 0;
}
扫描全部成绩,时间复杂度为O(n×m);只保存少量变量,额外空间为O(1)。
输出顺序是“列、行”,不是“行、列”。分母用行数n,而不是列数m;列的奇偶性决定上下方向。即使样例碰巧行列相等,也必须用不同的行列数再检查一次。
03
PART
拿纸算一遍,再看答案
CHECK · 三组小自测
看完解析后,不急着复制代码。试着自己回答下面的问题,确认理解的是规律,而不只是记住了某一行公式。
自测1|0与重复数字
输入字符串为 a7b070c2,《拼数》应该输出什么?需要保留几个0、几个7?
自测2|恰好坐满一列
《座位》考场有4行3列,某位考生排名第8。应输出哪两个数?如果排名是9呢?
自测3|只有一行
考场有1行5列,小 R 排名第4。偶数列向上是否还会改变他的行号?
答案与简析
① 输出 77200。两个7、两个0都要保留,再加上一个2。重复出现不等于只能选一次。
② 第8名输出 2 1;第9名输出 3 1。第8名恰好是第2列的最后一位,第9名才进入新列。
③ 输出 4 1。只有一行时,奇数列和偶数列的行号都为1,方向变化不会改变结果。
完整代码按2025年原题要求使用文件读写。在要求标准输入输出的练习平台上,需按平台说明去掉对应的两行freopen;不能把一种提交方式机械照搬到所有环境。
///
LAST
基础题,练的是把逻辑写完整
SUMMARY · 会做,也要写稳
《拼数》提醒我们:读到“任意多个”后,还要说明为什么全部保留更优,再选择适合百万字符的实现。
《座位》提醒我们:先明确变量含义和编号起点,再检查奇偶列、整列边界和输出顺序。
两道题都不需要冷门算法,但也不是“知道名字就能满分”。先独立分析,再编写代码,最后用重复、最小值和临界位置验证,才是把基础题写稳的训练。
不要只问“我会不会”,也问“我有没有验证”。
NOI官网:CSP-J/S 2025题目及数据
田浩然制作
把方法讲清楚,把程序写稳。