这是什么: 12 道 CSP-J 真题(2019–2025 年的 T1、T2,外加一道 T3)的三层提示,另有 3 道已在正课讲过的只给一行指引。
怎么用 —— 这份材料的用法比内容更重要:
⚠️ 提示 3 也不给完整代码,只给思路。代码必须自己写。 这不是为难你——你缺的从来不是代码,是"怎么想到"的那一步。抄一遍代码能过题,下次遇到同类题还是不会;把三层提示拆开看,练的才是那一步。
📝 每看一层提示,在纸上记一笔:我是卡在哪儿才看的。 第 7 次课复盘时,这张记录比任何讲义都值钱。
⚠️ 本册这 12 道题的完整标程在 S10_标程对照册.md
里,但那本有门槛:这道题你至少交过一次,才准翻。
没交过就去看标程,你拿走的是代码、丢掉的是"怎么想到"——S8 和
S10 分开成两个文件,就是为了让你不容易顺手翻过去。
关于 freopen: 在洛谷上提交时不要写 freopen(洛谷用标准输入输出)。本地按考场格式练习时再加上,文件名自己定即可——真正的考场文件名以当年题面为准。
按"能立刻上手 → 需要新知识 → 需要想通一层"排的,不要跳着做。
| 顺序 | 题 | 年份题号 | 需要什么 |
|---|---|---|---|
| 1 | 数字游戏 | P5660 | L01(走通流程用) |
| 2 | 扑克牌 | P11227 | S3 计数数组 |
| 3 | 优秀的拆分 | P7071 | S1 位运算 |
| 4 | 拼数 | P14357 | S3 计数数组 |
| 5 | 分糖果 | P7909 | S5 取模 |
| 6 | 座位 | P14358 | L02 排序 |
| 7 | 地图探险 | P11228 | L06/L07 方向数组 |
| 8 | 直播获奖 | P7072 | S3 计数数组 |
| 9 | 插入排序 | P7910 | S2 稳定性 |
| 10 | 公交换乘 | P5661 | S4 vector |
| 11 | 解密 | P8814 | S5 gcd/sqrt 精度 |
| 12 | 一元二次方程 | P9750 | S5 gcd(T3,行有余力) |
已经在正课讲过的三道,不重复给提示:
L01_复赛规则与枚举模拟.md 例题一L01_复赛规则与枚举模拟.md 例题二L02_排序与贪心.md 本课练习给一个长度为 8 的 01 字符串,输出其中字符
1的个数。
输入是字符串,不是数字。别想着用取模拆位——直接用
string 读进来,一个字符一个字符看。
判断条件是 s[i] == '1'——单引号的字符
'1',不是数字 1。这是 L05 讲过的 char 与 int
的区别。
读入 string s,for
遍历每个字符,if (s[i] == '1') cnt++,输出
cnt。
这题本身没有任何难度。它的真正用途是让你把整套流程完整走一遍:建题目文件夹、写
freopen、造 .in 文件、检查
.out。如果这一步就卡住,先回去看
freopen函数用法教程.md——考场上这套流程出错,题做对了也是 0
分。
小 P 手上有 n 张牌(可能重复),每张牌是花色 + 点数。一副完整的牌是 4 种花色 × 13 种点数 = 52 张,每种恰好一张。问最少还要再借几张才能凑齐一副。
先把问题翻译一下:答案 = 52 − (手上不同牌的种数)。重复的牌一点用都没有。
所以真正要算的是"去重之后有几张"。
每张牌是两个字符:花色 ∈ D C H S,点数 ∈
A 2 3 4 5 6 7 8 9 T J Q K。
问题变成:怎么把一张牌变成数组下标? 想想 S3 里"拿数值当下标"的做法——你需要把字符映射成 0 开始的编号。
开 bool vis[4][13](全局,自动清零)。
映射办法:把 "DCHS" 和 "A23456789TJQK"
各存成一个字符串,用 find 查出字符的位置就是编号。例如
string suits = "DCHS"; int a = suits.find(s[0]); 得到
0~3。点数同理得到 0~12。
读一张就 vis[a][b] = true。最后双重循环数有多少个
true,答案 = 52 - 个数。
⚠️ 用 cin >> s 读一个长度为 2 的
string,别用 char
一个个读——空格和换行会给你惊喜。
把正整数 n 拆成若干个互不相同的、2 的正整数次幂之和,从大到小输出这些数;拆不出来输出
-1。注意"正整数次幂"是 2¹, 2², 2³…,不含 2⁰ = 1。
2 的正整数次幂是 2, 4, 8, 16, 32…,它们有一个共同点。
想清楚这个共同点,你就能立刻判断出哪一类 n 一定拆不出来。先把这件事想明白,再想怎么拆。
它们全是偶数。若干个偶数相加还是偶数,所以 n 是奇数就直接输出 -1。
n 是偶数时呢?想想 S1:任何正整数写成二进制后,就是若干个不同的 2ᵏ 相加。而 n 是偶数保证了它二进制的第 0 位是 0——也就是永远不会用到 2⁰,正好符合题目要求。
所以:拆分方式就是 n 的二进制表示,而且它唯一。
if (n % 2 == 1) { 输出 -1; return 0; }if ((n >> k) & 1) cout << (1 << k) << " ";⚠️ 题目要求从大到小输出,所以 k 必须从大往小循环。 ⚠️ 位运算记得套括号(S1 坑 1)。
给一个只含小写字母和数字的字符串 s(保证至少有一个 1~9 的数字)。从中挑出任意多个数字字符(每个位置最多用一次),重新排列,拼出最大的正整数。
两个问题按顺序问自己:
于是答案就是:把 s 里所有数字字符降序排一遍,直接输出。
用 S3 的计数数组:int cnt[10],扫一遍 s,是数字就
cnt[s[i] - '0']++。
然后 for (int v = 9; v >= 0; v--),每个 v 输出
cnt[v] 次。
⚠️ |s| 可以到 10⁶。不要用 ans = ans + 字符
这样一点点拼字符串——每次拼接都在复制整个字符串,会超时。要么直接
cout 输出(并在 main 开头加
ios::sync_with_stdio(false);),要么先把结果填进一个
char 数组再一次输出。
n 个小朋友(包括你)。你拿 k 颗糖,L ≤ k ≤ R。每轮所有 n 个人各拿一颗,直到剩下的不够分给每个人,剩下的糖归你。求你最多能得到多少颗。数据范围 2 ≤ n ≤ L ≤ R ≤ 10⁹。
先把题意翻译成一个式子:你最后得到的是
k % n。所以要求的是 k 在 [L, R]
里取值时,k % n 的最大值。
k 能到 10⁹,所以绝对不能把 k 从 L 试到 R(回想 L01 的复杂度预算表)。
那就换个问法:k % n 最大能是多少?什么样的 k
能取到那个最大值?
k % n 的上限是 n - 1,在 k 恰好是"n
的倍数减 1"时取到。
所以问题变成:区间 [L, R] 里存不存在这样的数?
这等价于问:L 和 R 是不是落在同一个"长度为 n 的段"里。段的编号怎么算?见 S5 的第二条 📝。
L / n != R / n:区间跨过了至少一个 n 的倍数,那么"n
的倍数减 1"一定在区间内,答案 n - 1。k % n 随 k
单调递增,答案 R % n。整道题就一个 if。用题目样例验算一遍再交:
n 行 m 列的考场,n×m 名同学按成绩从高到低"蛇形"入座:最高分坐第 1 列第 1 行,然后沿第 1 列往下坐;第 1 列坐满后转到第 2 列,从下往上坐;奇数列往下、偶数列往上,依此类推。给定所有人的成绩(第一个是 R 的成绩,成绩互不相同),求 R 坐在第几列第几行。n, m ≤ 10。
拆成两个独立的小问题:
两个问题分开解决,别混在一起想。
第 1 问:成绩互不相同,所以根本不用排序——直接数"有多少个人的成绩比 R 高",加 1 就是名次。
第 2 问:每列坐 n 个人。第 1~n 名在第 1 列,第
n+1~2n 名在第 2 列……所以列号由 (pos-1) / n
决定。至于行号,要分奇数列(从上往下)和偶数列(从下往上)两种情况。
设 pos 为名次(从 1 开始):
c = (pos - 1) / n + 1t = (pos - 1) % n + 1r = t(从上往下)r = n - t + 1(从下往上)输出 c 和 r。
n, m ≤ 10,怎么写都不会超时——这题考的纯粹是"把规则翻译成公式"。写之前先在纸上画一个 3 行 3 列的表,把 1~9 号填进去,再拿公式逐个验证:pos=4 应该落在第 2 列第 3 行,pos=6 应该落在第 2 列第 1 行。对上了再动手。
📝 实在推不出公式也有退路:n, m ≤ 10 意味着最多 100 个座位,直接开个二维数组按规则一个个填进去,填完再找 R 在哪。慢是慢,但一样满分——这就是 L01 说的"暴力是你的朋友"。
n×m 网格,
.是空地、x是障碍。机器人有位置 (x, y) 和朝向 d(0=东 1=南 2=西 3=北),从 (x₀, y₀) 朝 d₀ 出发,执行 k 次操作:算出前方格子,若在界内且是空地就走过去,否则原地右转(d = (d+1) mod 4)。问整个过程中一共到过多少个不同的格子。多组数据,T ≤ 5,n, m ≤ 10³,k ≤ 10⁶。
⚠️ 这题不是搜索。 别看见网格就上 DFS/BFS——机器人的路线是完全确定的,没有"选择",你只要照着规则老老实实走 k 步。
这叫模拟,是 L01 的内容。搜索是"试所有可能",模拟是"照做"。
方向数组照搬 L06/L07,但顺序必须严格按题目的编号排:0=东、1=南、2=西、3=北。
东是列 +1,南是行 +1,西是列 −1,北是行 −1,所以
dx[4] = {0, 1, 0, -1}、dy[4] = {1, 0, -1, 0}。
排对了之后,"右转"就是漂亮的一行:d = (d + 1) % 4。
开 bool vis[1005][1005]
记录到过的格子,起点也要标记并计数。
循环 k 次:
nx = x + dx[d]; ny = y + dy[d];nx, ny 在界内 且 是空地 →
x = nx; y = ny;,若 !vis[x][y] 则标记并
cnt++d = (d + 1) % 4⚠️ 多组数据(T ≤ 5),每组开始前 vis
必须清零,否则上一组的痕迹会污染这一组。这是多测题最常见的死法。
⚠️ k ≤ 10⁶,直接模拟 10⁶ 步完全来得及,不需要找循环节。
逐个公布 n 名选手的成绩。每公布一个就要输出一次实时获奖分数线:若已公布 p 个成绩,获奖人数定为
max(1, ⌊p × w%⌋),分数线就是排名前这么多名中的最低分。所有成绩都是不超过 600 的非负整数,n ≤ 10⁵,1 ≤ w ≤ 99。
最直接的想法:每公布一个成绩,就把已公布的所有成绩排个序,取第 k 名。
先算一下这个做法的复杂度,再对照 L01 的预算表看看 n = 10⁵ 时过不过得去。
(算完你会发现过不去。那就回头找题面里被你忽略的那句话。)
每次重排是 O(n log n),做 n 次就是 O(n² log n),远超 10⁸。
被忽略的那句话是:"所有成绩都是不超过 600 的非负整数"。值域只有 601 个值——这是在明示你用 S3 的计数数组。
开 cnt[0..600],公布一个成绩就
cnt[x]++,完全不需要排序。那怎么求第 k
名的分数?从 600 往下累加个数,累加值第一次 ≥ k 时的那个分数就是。
维护 int cnt[605],已公布个数 p。每读入一个成绩 x:
cnt[x]++; p++;int k = max(1, p * w / 100); ⚠️
纯整数运算——题面专门提醒过,写成
p * w / 100,不要出现 0.01 或
double,浮点误差会让你在某些点上差一名sum += cnt[i],一旦
sum >= k 就输出 i 并
break复杂度:单次扫最多 601 步,总共 601 × 10⁵ ≈ 6×10⁷,稳过。
📝 这就是 S3 里说的"边加边查":计数数组真正的价值不是排序快,而是新来一个数只要 O(1) 更新,不用把整个序列重排。
给数组 a₁…aₙ 和题面中的插入排序伪代码。支持两种操作:① 把某个位置的值改成 v;② 询问"执行插入排序后,原来第 x 个元素排到了第几位"。n ≤ 8000,Q ≤ 2×10⁵,其中类型一(修改)最多 5000 次。
先把修改和查询都放一边,只回答一个问题:
不真的去跑那段插入排序,你能直接算出"原来第 x 个元素最终在第几位"吗?
(提示的提示:去读一遍题面给的伪代码,注意它的交换条件是"严格小于"还是"小于等于"。这个细节是整道题的钥匙。)
伪代码只在严格小于时才交换 →
两个相等的元素永远不会互换 → 这个插入排序是稳定的(见
S2)。
所以"插入排序后的位置"完全等价于:按 (值升序, 原下标升序) 双关键字排序后的排名。
问题于是变成一个干净的形式:维护一个序列,支持"改某个值"和"查某个元素的排名"。
关键在读懂这句数据范围:Q 到 2×10⁵,但修改最多只有 5000 次。
出题人是在告诉你:修改可以很慢,查询必须很快。
所以做法是:维护一张表 pos[i] = 原第 i
个元素当前排在第几位。
pos[x],O(1)pos 表重算的办法:把 (值, 原下标) 存成结构体数组,用 S2
的双关键字 cmp 排一遍,然后
pos[b[j].idx] = j + 1。修改只影响一个元素,所以更省的做法是只把那个元素在有序序列里挪到正确位置(O(n)
的一次插入),不必整个重排。
⚠️ 先把"每次修改都整个重排"的版本写对,确认样例过了、拿到部分分,再去优化。这是 L01 的部分分策略。
坐地铁会得到一张优惠券:45 分钟内有效,可以免掉一次票价不超过该地铁票价的公交车费。券会累积。坐公交时若有可用的券就用掉(优先用最早得到的),否则正常付钱。给出按时间顺序的乘车记录,求总花费。n ≤ 10⁵,票价 ≤ 1000,时间 ≤ 10⁹。
先把规则拆成两件互不相干的事:
分开写,别混在一起。
"可用"要同时满足三个条件,一个都不能漏:
t_公交 − t_地铁 ≤ 45券的面值 ≥ 公交票价题目说优先用最早得到的,而记录本来就按时间顺序给你,所以"从前往后找第一张满足这三条的券"就是对的。
用三个数组(或一个 vector
存结构体)按产生顺序记下每张券的:面值、时间、是否已用。
坐公交时从前往后扫,找到第一张满足三条件的就标记为已用、这次不花钱;扫完没找到就
ans += 票价。
⚠️ 这个写法是 O(n²),n = 10⁵ 会超时。但它能拿到 30% 的部分分(n ≤ 1000 那一档)——先把它写对,这是 L01 说的"先保证暴力分到手"。
优化方向:维护一个"起始指针" head。因为记录按时间递增,一旦某张券对当前这次公交已经过期,它对后面所有次公交也永远过期,可以让 head 直接跳过它,再也不看。
⚠️ 注意区分两种跳过:过期的券可以推 head 永久跳过;已用但没过期的券不能推 head 越过去(它后面可能还有没用的券)。
给定 n、e、d,求正整数 p、q 满足
n = p × q且e × d = (p−1)(q−1) + 1,输出时保证 p ≤ q;无解输出NO。n ≤ 10¹⁸,e×d ≤ 10¹⁸。
两个未知数 p、q,两个方程。这是纯代数题,不是算法题。
第一个方程直接给了你 p × q = n。
现在把第二个方程展开:(p−1)(q−1) + 1 = pq − p − q + 1 + 1。你已经知道
pq 是多少了——能不能解出 p + q?
e×d − 1 = pq − p − q + 1 = n − (p+q) + 1
移项得 p + q = n − e×d + 2(题面提示里那个 m 就是它)。
现在你同时知道了两个数的和 m 与积 n。那么 p、q 就是一元二次方程
x² − m·x + n = 0
的两个根。剩下的就是解方程 + 判断解合不合法。
m = n - e*d + 2Δ = m*m - 4*n,若 Δ < 0 → 输出
NOs = √Δ,若 s*s != Δ(不是完全平方数)→
输出 NOp = (m - s) / 2,q = (m + s) / 2p * q == n。不满足就 NO两个必踩的坑:
⚠️ 全程 long long。 n 到
10¹⁸,m*m 和 4*n 都在 long long
的边缘(上限约 9.2×10¹⁸),int 连门都摸不到。
⚠️ 不能直接用 sqrt。
这是本题最大的杀手:sqrt 返回 double,只有约
15~16 位有效数字,而 Δ 有 18 位——算出来会差
1,于是"是不是完全平方数"的判断直接错掉。必须用 S5
里那个"先算再修"的写法:先 (long long)sqrt((double)Δ),再用
while 往上下各修一修。
⚠️ 步骤 4 里 m - s 可能是奇数(除以 2
除不尽),也可能是负数。第 5
步的回代验证能兜住这些情况——别省这一步。
给整数系数 a、b、c(a ≠ 0),判断 ax² + bx + c = 0 有没有实数解。无解输出
NO;有解则按规定格式输出较大的那个根:有理根输出最简分数p或p/q;无理根输出q₁+q₂√r的形式,其中 q₁、q₂ 是有理数、q₂ > 0、r 无平方因子且 r > 1。输出中不能有任何空格。
⚠️ 这是 T3,而且它的难度 90% 在输出格式,不在数学上。 求根公式你早就会了。
所以第一步不是写代码,是拿张纸把所有情况列全:
NOq₁+q₂√r 形式(q₁ 为 0
时还输不输出;q₂ 为 1 时输不输出这个 1)每一种情况先在纸上写出它对应的输出长什么样。 列不全就开始写代码,一定会陷进无穷无尽的 WA。
较大根 = (-b + √Δ) / (2a)。
⚠️ 但 a 可能是负数! a < 0
时,(-b + √Δ)/(2a) 反而是较小的那个根。先统一处理:若 a
< 0,把 a、b、c 三个全部取反(方程的根完全不变),这样就能保证 a >
0,后面所有分析都简单一档。
有理根的情况:把结果当分数 p / q
处理,用 S5 的 gcd 约到最简,保证分母为正;分母为 1
时只输出分子。
无理根的情况,要把 √Δ 化简成 q₂√r
的形式,r 无平方因子:
枚举 i 从 2 到 √Δ,只要 i*i 整除
Δ,就从根号里提出一个 i(Δ /= i*i,提出的系数
*= i),同一个 i
要反复提到提不动为止。剩下的 Δ 就是 r。
然后 x = -b/(2a) + (提出的系数/(2a))·√r,两个分数各自用
gcd 约分。
按提示 1 列的表,逐种情况拼输出字符串。
⚠️ 题面要求 q₂ > 0,且输出不含空格。 ⚠️ T ≤ 5000 组数据,但 |a|,|b|,|c| ≤ 10³,所以 Δ ≤ 约 4×10⁶,枚举提平方因子完全来得及。
📝 建议这道题最后做,而且做之前先把前 11 道全部 AC。 它的调试成本远高于收益——考场上遇到这种题,先把 T1、T2 拿满再回头。