可达信奥 · 考场代码诊断
CSP-S 2026 第二轮 · 提交编号 HA-S00419 · 实测于 2026-11-06

四题 0 分。
先看清楚,这是哪一种 0 分。

四份代码逐行读完:框架都写对了——freopen、读入循环、输出格式,一样不差;缺的是中间那一段算法。唯一像在解题的 T1,靠末尾一个 \(\text{num}+1\) 把两个样例凑对了。所以这份报告不写"应得多少分",只讲浩然现在会什么、接下来补什么。

学生何浩然
代码包crystal / subway / migrate / hub
场次CSP-S 2026 第二轮(提高组)
官方样例2 / 2 通过(仅 T1)
四题得分0 / 400
crystal — 考场源码 · 逐行阅读
$ g++ -O2 crystal.cpp -o crystal编译通过
$ ./crystal ← 官方样例 13样例对
$ ./crystal ← 官方样例 24样例对
$ ./crystal ← \(n=1\), \(m=0\)0应为 1
— 断点看第 24 行前的 num:样例 1 是 2,样例 2 是 3
— 末尾 num + 1:2 → 3、3 → 4,样例就是这样"对"上的
官方评测:T1 = 0 / 100
四题得分
0/ 400
四题合计
官方样例
2/ 2
仅 T1 两行对上了
算法主体
0处
没有前缀和 / 最短路 / BFS / DP
实现级硬错
6处
下标 · 区间 · 上界 · 越界 · 规模 · 逗号
最容易被误判的一处

样例 2 / 2 全过,靠的是末尾一个 num + 1

断点看第 24 行之前的计数器:样例 1 里 num = 2,样例 2 里 num = 3。浩然最后写 if (num != 0) cout << num + 1;,2 补成 3、3 补成 4——两个样例的"正确",是这个 +1 凑出来的,不是算出来的。同一个程序在 \(n=1\)、\(m=0\) 时 \(\text{num}=0\),直接输出 0,而正确答案至少是 1。

01

四份代码的骨架

按「读入 → 处理 → 输出」逐份过一遍,看中间那段是谁

先不谈分数,看结构。四份程序其实做了同一件事:读一点数据,打几行输出,中间那一段本该是算法的地方是空的。这不是四道题的四个问题,是同一个问题的四次出现。

T1 crystal
31 行
读入a[1..n],格式正确
→
处理配对计数,不是题意
→
输出样例靠 num+1 对上
唯一一份看得出"在解题"的代码,但算的东西不是题目要的;而且第 13 行的 sort 从未生效。
T2 subway
19 行
读入格式错,只读 m 个单值
→
处理没有建图,没有最短路
→
输出写死 "32 / 17"
w[]、ww[] 读进来以后再没被用过一次;第 10 行的 \(\texttt{long long u = n}\) 声明完也没用。
T3 migrate
30 行
读入cin >> a[i],下标用错
→
处理四条特判,坐标不再使用
→
输出行数随 m 变化
坐标读进来直接丢掉,输出只由第二行的 m 决定:\(m=2\) 吐三行、\(m=3\) 吐一行、其余一行不吐。数组还只开到 \(10^{6}+10\)。
T4 hub
9 行
读入没有任何输入语句
→
处理无
→
输出恒定输出 1 / 2
这是"整题放弃"的代码形态,不是"写错了"。
01
排序区间只含一个元素:crystal.cpp 第 13 行\(\texttt{sort(a, a+1)}\) 是左闭右开,只覆盖 \(a[0]\);而 \(a[0]\) 这个位置一直空着,所以整份数组一次都没排过。
02
循环上界取了操作次数:crystal.cpp 第 14、16 行上界是 m(最多 \(10^{18}\)),不是 n(最多 \(5\times10^{5}\))。m 还在第 19 行被 \(m\gets m-s\) 改掉,循环变成边跑边改。
03
越界比较:crystal.cpp 第 17 行j 走到 m 时读 \(a[j+1]\),数组只有 \(10^{6}+10\) 个位置,后面全在界外。
04
内层读入用了外层下标:migrate.cpp 第 14 行\(\texttt{cin >> a[i]}\) 里的 i 是测试组序号,应该是 j。n 个坐标全落进 \(a[1]\)。
05
逗号表达式当成"且":migrate.cpp 第 16、19、22、25 行\(\texttt{if (n == 4, m == 2)}\) 等价于 \(\texttt{if (m == 2)}\),n 的检查被求值后丢掉。
06
数组按小规模开:四份代码第 3 行都写 \(N=10^{6}+10\);T3 题面 \(n\le 2\times10^{6}\),直接不够。

六处里有五处是写完之后一眼能看出来的语法级问题,只有 T1 的算法方向属于"还没学会"。这个比例说明一件事:浩然不是学不会,是还没人教过"写完怎么读一遍自己的代码"。所以下面不再估算"应得多少分",只讲现在会什么、下一步练什么。

02

逐题病历

每题:代码原文 → 它实际做了什么 → 错在哪一行

四张卡按「离学会的距离」排:T1 最接近,T4 最远。每张都贴了浩然自己的代码,不是重写的版本。

T1

能量水晶

crystal 思路偏了,样例是凑出来的 0/ 100
题意:n 颗水晶,最多 m 次「把某颗 +1」,求最多几颗能变成同一个值。\(n\le 5\times10^{5}\),\(m\le 10^{18}\),能量值可为负。
浩然当时在算什么:先排序,再对 \(s=0\ldots m-1\) 找满足 \(a[j]+s=a[j+1]\) 的相邻对,每命中一对 \(\text{num}\gets\text{num}+1\)。而题目要的是「选一个目标值,看一个区间里有几颗数能补到它」——「数配对」和「数个数」是两件事,这个 num 无论怎么算都推不出正确答案。
错在哪几行:① 第 13 行 \(\texttt{sort(a, a+1)}\) 只覆盖 \(a[0]\),数组从未排序,「相邻」这个前提不成立;② 第 14、16 行循环上界是 \(m\)(最长 \(10^{18}\)),不是 n(最长 \(5\times10^{5}\));③ 第 17 行 j 走到 m 时读 \(a[j+1]\) 越界;④ 第 19 行 \(m\gets m-s\) 在循环里改上界,第 15 行 \(s=i\) 在 i 超过 \(2^{31}\) 后溢出;⑤ 最要命的一条——末尾的 \(\text{num}+1\) 是拿输出反推出来的。
浩然的原文(crystal.cpp 第 13–23 行)
sort(a, a + 1); // 第 13 行:区间只含 a[0] for (int i = 0; i < m; i++) { // 上界是 m,不是 n int s = i; for (int j = 1; j <= m; j++) { // j = m 时 a[j+1] 越界 if (a[j] + s == a[j + 1]) { // 数的是"配对",不是"能凑几颗" num++; m -= s; // 循环上界被就地改掉 break; } } } if (num != 0) { cout << num + 1; ... } // 第 24 行:这个 +1 从哪来?
官方样例与最小规模输入上的实际输出
输入正确答案输出说明
官方样例 133num = 2,加 1 才对
官方样例 244num = 3,加 1 才对
n = 1,m = 0(题面允许)10m = 0 时循环一次不进
$ 断点停在第 24 行之前,看 num
num = 2      ← 样例 1;+1 之后输出 3
num = 3      ← 样例 2;+1 之后输出 4
$ 再跑 n=1, m=0
0         ← 应为 1:一颗水晶自己就成一组
这道题该怎么想:先定目标值,再数区间(\(O(n\log n)\))
sort(a + 1, a + n + 1); for (int i = 1; i <= n; i++) pre[i] = pre[i-1] + a[i]; int l = 1, ans = 1; for (int r = 1; r <= n; r++) { // 把 [l, r] 全部补到 a[r],需要多少步 while (l < r && (long long)(r-l+1) * a[r] - (pre[r] - pre[l-1]) > m) l++; ans = max(ans, r - l + 1); // 区间长度 = 能凑成同一值的颗数 } cout << ans;
核心式子:把区间 \([l,r]\) 全部补到 \(a[r]\),要花的步数是 $$cost(l,r)=(r-l+1)\,a_r-\sum_{i=l}^{r}a_i$$ 前缀和让这个代价 \(O(1)\) 就能算出来,l 才不必回退——这也是为什么"数配对"永远得不到答案:浩然要的量是这个区间的长度,不是配对的次数。
这一课的重点不在算法,在"读自己的输出"。写出 \(\text{num}+1\) 的那一刻,其实已经发现结果差了一点;正确反应是回去问「num 到底在数什么」,而不是给结果加一。加一能让样例变对,也能让真正的错藏得更深——这是浩然这次最该改的习惯。
知识点:枚举目标值 + 前缀和 + 双指针易错:sort 区间易错:循环上界取非规模变量易错:用输出反推答案
T2

地铁

subway 没进入题目 0/ 100
题意:地铁上不花步行时间,只有进出站、换乘花时间;每次查询求两点间最少总步行时间。线路数 \(n\le 500\),每站最多 \(d\le 20\) 条线,查询 \(q\le 10^{5}\)。
浩然当时在做什么:读 c、n、m、d、q 五个数,按 m 次读 \(w[\ ]\)、按 n 次读 \(ww[\ ]\),然后把 "32" 和 "17" 打出去。这两个数正是样例 1 的答案。
两处都没走通。读入:题面每条线路先给 \(k_i\),再给 \(2k_i\) 个数(站号、入站时间交替),浩然只按「m 个单值 + n 个单值」读,格式整个错位。处理:w[] 和 ww[] 读进来以后一次都没被用过,第 10 行的 \(\texttt{long long u = n}\) 也没用过——程序里没有建图、没有最短路、没有循环处理那 q 组查询。
$ 在 subway.cpp 里搜 w 的全部出现位置
第 4 行   long long w[N], ww[N];     ← 声明
第 12 行  cin >> w[i];                ← 读入
第 15 行  cin >> ww[i];               ← 读入
此后无任何引用                    ← 数据进来就断了
$ ./subway  ← 任意输入
32
17                                  ← 恒等于样例 1 的两行答案
第一步不是建图,是"把输入读对并原样打印"
struct Line { int k; int st[1005], t[1005]; } L[505]; for (int i = 1; i <= n; i++) { cin >> L[i].k; // 每条线路先读站数 kᵢ for (int j = 1; j <= L[i].k; j++) cin >> L[i].st[j] >> L[i].t[j]; // 再读 2kᵢ 个数:站号 + 入站时间 } // 先只做这一件事:把读到的内容原样打印,确认格式真读对了 for (int i = 1; i <= n; i++) for (int j = 1; j <= L[i].k; j++) printf("line %d stop %d time %d\n", i, L[i].st[j], L[i].t[j]);
这一课:把"读进来"当成一道独立的题。读不对,后面写什么都白写。等浩然能在 10 分钟内把一条线路的 \(k_i\)、站号、时间装进结构体并原样打印,再讲「线路当点、换乘当边」的建模——那才是这题的第二层。
知识点:输入格式解析知识点:结构体存变长序列后续:线路当点的建图后续:Floyd / 分层图 Dijkstra
T3

迁徙

migrate 特判一条也没生效 0/ 100
题意:坐标严格递增的 N 个点,两点坐标差 \(\le d\) 时可直飞;统计「最短路恰好有两条」的点对数量。\(n,N\le 2\times10^{6}\),\(d\le 2\times10^{18}\)。
浩然当时在做什么:读 c、t,然后每组读 n、m 和 n 个坐标,接着按 (n, m) 的四种组合各写一条 if,想用小数据特判骗点分。
两处硬伤,同时发作。① 第 14 行 \(\texttt{cin >> a[i]}\):这里的 i 是外层"第几组数据",应该是内层的 j——n 个坐标全塞进 \(a[1]\),而且读完就再也没用过。② 第 16、19、22、25 行的 \(\texttt{if (n == 4, m == 2)}\) 是逗号表达式,等价于 \(\texttt{if (m == 2)}\),n 的检查被求值后丢弃。③ 数组 \(N=10^{6}+10\),题面 n 可达 \(2\times10^{6}\),直接不够。
if ( n == 4 , m == 2 ) 求值一次,结果丢掉 这个才是真正的条件 n 只在左边被读了一下,从此不参与任何判断。
$ 按代码直接推,每组数据的输出只由 m 决定
m == 2   → 第 16、19、25 行都命中 → 输出 1 / 2 / 1(三行)
m == 3   → 只有第 22 行命中       → 输出 0(一行)
其他 m   → 四条 if 全不命中        → 输出文件里一个字都没有
正确答案是每组一行、共 t 行。三行、一行、零行,三种情况都对不上。
计数这件事只有一条规则:同一个距离层级上才累加。 $$dist[v]=dist[u]+1\ \Longrightarrow\ cnt[v]\gets cnt[v]+cnt[u]$$ 第一次走到 \(v\) 时直接继承 \(cnt[u]\);之后若再发现 \(dist[v]\) 恰好等于 \(dist[u]+1\),说明又找到一条同样长的路,把方案数加上去。
先把两行写对:读入下标 + 条件连接
for (int j = 1; j <= n; j++) cin >> a[j]; // 内层读入用内层变量 j if (n == 4 && m == 2) { ... } // 多个条件一律 && // 再往上一层:枚举起点 BFS,记最短路长度与条数 dist[s] = 0; cnt[s] = 1; if (dist[y] == -1) { dist[y] = dist[x] + 1; cnt[y] = cnt[x]; q.push(y); } else if (dist[y] == dist[x] + 1) cnt[y] += cnt[x]; // 同层累加方案数 if (dist[v] != -1 && cnt[v] == 2) ans++;
这一课:特判骗分的前提是"特判条件写对"。四条分支被一个逗号压成一条半,连"骗"都没骗成。而这四条分支,连"答案应该有几行"都没核对过——交卷前把自己的输出行数数一遍,这个错误当场就会暴露。
知识点:最短路计数(dist + cnt 双数组)易错:逗号运算符易错:内层读入用外层下标易错:数组按小规模开
T4

研发基地

hub 这题没有动手写 0/ 100
题意:n 个点的树,选恰好 k 个点、形成不超过两个连通块,使所有点到最近选中点的最大距离最小。
这道题要最小化的量其实是一个式子:在 \(|S|=k\)、且被选中的点分成不超过两块的前提下, $$\min_{S}\ \max_{1\le v\le n}\ \min_{u\in S}\ \operatorname{dist}(u,v)$$ 正解是二分答案加树形 DP 检验;而 \(n\) 很小的一档,直接枚举 \(S\) 就能算出这个式子。
\(n\le 10^{6}\),\(t\le 5\)。
浩然当时在做什么:9 行。开了数组、写了 freopen,然后 cout << 1 << "\n" << 2;。c、t、n、k 一个都没读。
不是写错,是没写。整个程序里没有一条输入语句,输出与数据无关;那两个数恰好是样例 1 的答案,说明浩然是照样例交的。这题的正解(二分 + 树形 DP)对浩然确实超纲,但题面留了 \(n\le 5\) 和 \(n\le 10\) 两档,枚举子集 + BFS 就能落地——这一档浩然没试。
$ grep "cin" hub.cpp
(无结果)        ← 全文没有任何读入
$ ./hub  ← 任意输入
1
2              ← 恒等于样例 1 的答案
最小一档够得着:枚举子集 + 判连通块 + 求最远距离
for (int mask = 0; mask < (1 << n); mask++) { if (__builtin_popcount(mask) != k) continue; int comp = 0; // 选中点分成几块 for (int i = 0; i < n; i++) if ((mask >> i & 1) && !vis[i]) comp++, dfsBlock(i, mask, vis); if (comp > 2) continue; int far = 0; for (int v = 0; v < n; v++) far = max(far, nearest(v, mask)); ans = min(ans, far); }
这一课:不会做,也要让程序"读过题"。以浩然现在的水平(能写 for、能写 dfs),枚举这一档是做得到的。四份代码里这份最可惜——它连失败的机会都没给自己。
知识点:枚举子集知识点:连通块判定 + BFS正解:二分 + 树形 DP(超纲)现在够得着:\(n\le 10\) 那一档
03

把那一行 sort 放大看

可回放演示 · 上一步 / 下一步 / 自动播放

整份报告的起点就是这一个格子。点「下一步」,看排序区间到底圈住了谁——四步走完,你就明白为什么两个样例都过、评测却是 0 分。

1 / 4

这段话不看数据也成立:数组没排序时,\(a[j]\) 与 \(a[j+1]\) 只是"挨着",不是"最接近"。样例的两组输入本身升序,比较才碰巧成立——所以那两行"通过"证明不了任何事,只说明样例刚好没踩到这个坑。

04

下一步:从"能写循环"到"能写模型"

三步,每步只验收一个动作

不用说"要努力"。浩然的真实位置是:框架写对了(freopen、读循环、输出格式都对),中间那一段还没有。下面三步,每步只看一个动作有没有做到,做不到就停在这一步。

本周

把「读入」变成肌肉记忆

针对:T2 格式错位、T3 下标写错、T4 整题没读
  • 拿到任意一道题,先只做一件事:把题面给的每个数按格式读进数组或结构体,再原样打印一遍。
  • 用 T2 练:线路 → 结构体 → 打印,直到不看题面也能一遍写对。
  • 把 T3 里 \(a[i]\) 这种错,自己找出来一次(先不看这份报告)。
验收:给一份新题面,20 分钟内写出「读入 + 原样输出」,格式全对。
两周内

补三个最小模型

针对:T1 的方向、T3 的计数、T4 的枚举
  • 前缀和 + 双指针:T1 要用的就是它,重点理解"枚举目标值,看区间能补几个"。
  • BFS 求最短路 + 计数:T3 的最小一档,\(\text{dist}\) / \(\text{cnt}\) 两个数组。
  • 枚举子集:T4 的 \(n\le 10\) 档,\(\texttt{for (mask)}\) + dfs。
  • 每个模板手写三遍、不看原代码;写完自己造 3 组小数据验。
验收:三张模板能默写,并且能在 20 分钟内套进一道新题。
赛前

固定考场流程

针对:写死输出、行数不对、用输出反推答案
  • 拿到卷子先读四道题的样例,把每题「输出几行」写在草稿纸上。
  • T1 给 40 分钟,做完立刻自测 \(n=1\) 和极值;T2–T4 各留 20 分钟写最小一档。
  • 不会做也要交一版「读过输入」的代码;交卷前查三样:数组够不够、条件是不是 &&、下标对不对。
验收:模拟赛四道题都交出了「读过输入」的代码,且 T1 能过样例并自测通过。
考场交卷前的 8 条自检0 / 8
一个。区间 [a, a+1) 只含 \(a[0]\),而 \(a[0]\) 是你从不使用的下标 0,它甚至不参与后面的比较。样例 1、2 的输入本身就是升序的,所以"相邻即最接近"碰巧成立——删掉 sort 输出不变,正好证明它从未生效。
因为 \(\text{num}+1\)。断点停在第 24 行之前:样例 1 里 num 攒到 2,样例 2 里 num 攒到 3;浩然写 \(\texttt{cout << num+1}\),正好变成 3 和 4。这个"正确答案"是被调出来的,不是算出来的。判据很直接:\(n=1\)、\(m=0\) 时 \(\text{num}=0\),程序输出 0,而正确答案至少是 1。
执行了,但结果被丢掉了。逗号运算符从左到右求值,只把最后一个值作为整个表达式的值。所以这段等价于 \(\texttt{if (m == 2)}\),n 从此不参与判断——四条特判因此塌成一条半:\(m=2\) 吐三行,\(m=3\) 吐一行,其余一行不吐。
哪也没去。全文只在第 4 行声明、第 12 / 15 行读入,之后再没出现一次。程序里唯一决定输出的是第 17 行——写死的 "32" 和 "17"。所以这道题不是"没写完",是"没开工":数据进了变量,没有任何一行代码用它。
三步,都跟"会不会"无关:① 按题面格式把输入读进来,再原样打印一遍,确认格式真的对了;② 手算样例,把答案怎么来的写在草稿纸上;③ 写最小一档的暴力(\(n\le 10\) 就枚举)。这三步做完,交上去至少是一份读过题的程序,而不是两行样例答案。会做是加分项,读过题是底线。
学情备注:这四份代码的文件名(crystal / subway / migrate / hub)对应的是 CSP-S 2026 提高组四题,不是 J 组;如果浩然实际参加的是 J 组,把 J 组代码发我,我按同一套方法再出一份。就这四题看,浩然的位置很清楚:程序框架、freopen、单层读入循环都会写,但"把一段逻辑抽象成算法"还没有建立。所以这份报告没有给"应得多少分":这份 0 分要改的不是态度,是方法,而方法是可以教的。先把读入和三个最小模型补上,再谈分数。