可达信奥 · 考场代码诊断
CSP-J 2026 第二轮 · 准考证号 GD-J05861 · 2026-11-06

201 分。
四题都写到了,有一题的答案
落在了别处。

四份代码我都调出来逐行看过。婉璐的基础比 201 分看上去要好:四个文件的读入全对,字符串比较、输出格式都写得干净,第四题还会用 deque 维护桌面。丢分的地方很集中,三处都差半步——travel 有两处判断没把过道算进去,string 把压缩串整个展开了,and 的输出文件名写成了 ans.out。最后这处跟算法没关系,改一个字母,这题在我们平台上能跑出 45 分。

学生宁婉璐
准考证号GD-J05861
场次CSP-J 2026 第二轮(普及组)
四题成绩85 · 68 · 0 · 48
总成绩201
成绩单 · NOI 报名系统
$ 打开 CSP-J 2026 第二轮 成绩与申诉
travel85两处判断没过道
string68压缩串被展开了
and 0输出写进了 ans.out
card 48复杂度超了
总成绩201
$ 在可达平台把 and 的输出文件名改对,重交一次
45这一处改动就是 45 分
总成绩
201/ 400
travel · string · and · card
最高一题
85
travel,只错了两处判断
归零的一题
1
and,跟算法无关
平台重测
45
改对文件名后 and 的得分
先说最可惜的一处

freopen("ans.out","w",stdout) —— 题面要的是 and.out

and 这题记 0 分。代码我看完了,读入、排序、相与都跑得下去,问题在第 9 行的输出文件名:题面要求写进 and.out,婉璐写成了 ans.out。评测机只读 and.out,读到一份空文件,这题就不给分。我在平台上把这一处改对重交,45 分。

01

四题的成绩和骨架

按读入、处理、输出三段看,问题在哪一段

先看分数:travel 85、string 68、and 0、card 48,总分 201。四题里没有一题是完全没思路的。下面按"读入 → 处理 → 输出"拆开看,问题基本都在中间一段——四份代码的读入全对,这个基础在我带的学生里算不错的。

T1travel85
T2string68
T3and0
T4card48
050100
每题满分 100。and 不是做得少,是写出去的文件没被评测机读到,整题不计分。
T1 travel
38 行
读入 · 对四个串一起读,车厢号、座位号都拿到
→
处理 · 偏过道这件事,两处判断里都没算
→
输出 · 对分段补零,拼起来正好四位
这题丢的 15 分,全在中间那两处判断上。
T2 string
36 行
读入 · 对n 段,每段的长度和字符都读进来
→
处理 · 崩把压缩串按长度真的拼成了一个字符串
→
输出 · 对数出来的"不对称对数"就是答案
想法对,做法挑了个最费劲的,数据一大就跑不完。
T3 and
33 行
读入 · 对n、m 和 n 个数,一遍读完
→
处理 · 方向不对取最大的 m 个数相与,不是正解
→
输出 · 写错文件ans.out,题面要 and.out
两层问题叠在一起:算法方向不对,输出又写错了文件。
T4 card
71 行
读入 · 对n、m 和序列,一次性读入
→
处理 · 方向对、代价错deque 抓对了,但每个起点都重扫
→
输出 · 对m 个答案,空格分隔
四题里最接近做出来的一道,卡在复杂度上。
01
过道同侧这一问判断写坏了travel.cpp 第 25 行条件写成 \((s_1[2]\ge\texttt{'A'}\ \&\&\ s_2[2]\le\texttt{'C'})\ ||\ (s_1[2]\ge\texttt{'D'}\ \&\&\ s_2[2]\le\texttt{'F'})\)。\(\texttt{'A'}\) 是最小的字母,\(s_1[2]\ge\texttt{'A'}\) 永远成立;\(\texttt{'F'}\) 是最大的,\(s_2[2]\le\texttt{'F'}\) 也永远成立。整条式子实际变成「\(s_2\) 在左 或 \(s_1\) 在右」,跨过道的两张座位会被判成同侧。
02
相邻这一问按字母差算,C 和 D 被当成挨着travel.cpp 第 32 行\(\texttt{abs}(s_1[2]-s_2[2])==1\)。C 和 D 的字符差正好是 1,可它们中间隔着过道,不是左右相邻;D 和 F 差 2,反倒是对的。
03
压缩串被整个展开了string.cpp 第 24–26 行\(\texttt{for}(j=1;j\le a_i;j++)\ s\mathrel{+}=c_i\),把 \(\sum a_i\) 个字符一个一个拼进 string。题面把输入压成 n 段,就是因为总长很大,这样拼放不下。
04
又复制了一份反向的串string.cpp 第 28 行\(\texttt{re}(s)\) 造了一个逆串,内存翻倍。展开已经撑不住,这一下只是让它更早崩。
05
输出文件名少一个字母and.cpp 第 9 行\(\texttt{freopen("ans.out","w",stdout)}\),题面要 \(\texttt{and.out}\)。评测机读不到结果,整题 0 分——平台重测改对后 45 分。
06
按位与的贪心方向不对and.cpp 第 22–29 行排序后取最大的 \(m\) 个数相与。按位与看的是二进制位,不是数值大小,正解是从高位往下逐位判。
07
每换一个起点就把大数组清一遍card.cpp 第 18 行外层 \(k=1\ldots n\),每轮 \(\texttt{memset}(f,0,\texttt{sizeof}(f))\) 把 \(3\times10^5\) 个 int 清零。光这一步就是 \(10^{11}\) 量级。
02

一道一道看

写到哪里、哪里对、错在哪一行

四张卡按丢分多少排,每张都贴婉璐自己的代码,有问题的行我标了底色,您可以对着屏幕上的原文件核。每张最后一段是我下周上课准备让婉璐怎么改。

T1

旅游

travel 两处判断漏了过道 85/ 100
给两个座位号:两位车厢号 \(c\),加三位座位号 \(s\)(前两位是排号,末位是 A–D 或 F,C 和 D 中间是过道)。要依次判断是不是同车厢、同排、过道同一侧、左右相邻,输出一个 4 位 01 串。
先说写得好的地方。四个串一次读进来,车厢直接用字符串比,没问题。输出是分段拼的:判不过就打 "0000"、"000"、"0",判过了打 "1",拼起来正好四位。这个细节说明格式婉璐是想清楚了的,不是碰巧对上。
两处判断都把过道漏掉了。第 25 行判"过道同一侧":\(\texttt{'A'}\) 是最小的字母,所以 \(s_1[2]\ge\texttt{'A'}\) 永远成立;\(\texttt{'F'}\) 是最大的,所以 \(s_2[2]\le\texttt{'F'}\) 也永远成立。这条式子实际起作用的是「\(s_2\) 在左 或 \(s_1\) 在右」,于是先落在 \(s_1\) 在右、\(s_2\) 在左的那些组合上:两张牌明明隔着过道,它答"同一侧"。接着第 32 行判相邻,用的是 \(\texttt{abs}(s_1[2]-s_2[2])==1\),而 C 和 D 的字符差正好是 1,这两种座位又被当成挨着坐。两处一起错,一条数据丢两位。
按婉璐的代码一行行推出来的结果
座位关系正确答案婉璐的输出结果
同车厢、不同排10001000对
同排、B 与 C(同侧相邻)11111111对
同排、A 与 C(同侧不相邻)11101110对
同排、D 与 C(先右后左,跨过道)11001111错:同侧、相邻都判反
同排、D 与 A(先右后左,跨过道且不相邻)11001110错:跨过道判成同侧
婉璐的原文(travel.cpp 第 11–36 行)
if(c1==c2){ cout<<"1"; f++; } // 同车厢 else { cout<<"0000"; return 0; } if(s1[0]==s2[0]&&s1[1]==s2[1]&&f==1){ cout<<"1"; f++; } // 同排 else { cout<<"000"; return 0; } if((s1[2]>='A'&&s2[2]<='C')||(s1[2]>='D'&&s2[2]<='F')&&f==2){ cout<<"1"; f++; // 第 25 行:跨过道也判成同侧 else { cout<<"00"; return 0; } if((int)abs(s1[2]-s2[2])==1&&f==3){ cout<<"1"; } else { cout<<"0"; } // 第 32 行:C 与 D 差 1
这题我打算让婉璐这么写:先把字母压成编号
int p1 = s1[2] - 'A' - (s1[2] == 'F'); // A B C D F → 0 1 2 3 4 int p2 = s2[2] - 'A' - (s2[2] == 'F'); if (c1 != c2) cout << "0000"; else if (s1.substr(0,2) != s2.substr(0,2)) cout << "1000"; else if ((p1 <= 2) != (p2 <= 2)) cout << "1100"; // 一个在左、一个在右 else if (abs(p1 - p2) != 1) cout << "1110"; else cout << "1111";
这道题准备让婉璐重写一遍。先把 A 到 F 压成 \(0\ldots4\):过道左边是 \(0,1,2\),右边是 \(3,4\)。压完之后,同侧就是两边编号要么都 \(\le 2\)、要么都 \(>2\);相邻就是 \(|p_1-p_2|=1\)。题目里凡是"中间隔着什么"的说法,第一步都要变成能比较的数,这一步不做,后面怎么写都会漏。
知识点:字符串比较知识点:把字符压成编号易错:恒真条件易错:过道
T2

回文串

string 想法对,规模撑不住 68/ 100
字符串按 \(n\) 段压缩给出,第 \(i\) 段是 \(a_i\) 个字符 \(c_i\)。每一次可以改任意一个位置上的字符,问最少改几处能让整串变成回文串。
想法是对的。把"最少改几处"换成了"数一数有多少对对称位置不一样",一对不一样改一个就行。第 32 行的循环也只扫了前半段,正中间那个字符没有重复算。这个转化跟正解是一个路子,能想到这一步,说明回文这类题婉璐是理解的。
问题在数这一对数的方法上。为了把对称位置找出来,婉璐把 \(\sum a_i\) 个字符真拼成了一个字符串(第 26 行 s += c[i]),又用 re(s) 复制了一份倒过来的(第 28 行)。题面把输入压成 \(n\) 段,本身就是在说总长很大:这么拼,内存放不下,时间也过不去。68 分是那些总长还小的数据给的。
婉璐的原文(string.cpp 第 20–34 行)
for(int i=1;i<=n;i++){ cin>>a[i]; cin>>c[i]; for(int j=1;j<=a[i];j++){ s+=c[i]; // 第 26 行:把压缩串展开成实体 } } string ss=re(s); // 第 28 行:又复制一份,内存翻倍 for(int i=0;i<s.size()/2;i++){ if(s[i]!=ss[i]) sum++; // 数的是"不对称对",这一步对 }cout<<sum;
正解:不展开,在段上走
int l = 1, r = n, p = a[1], q = a[n], ans = 0; while (l < r) { int len = min(p, q); // 这一轮最多能配成多少对 if (c[l] != c[r]) ans += len; // 字符不同:这一轮的 len 个位置都得改 p -= len; q -= len; if (p == 0) p = a[++l]; // 左块用完,换下一块 if (q == 0) q = a[--r]; // 右块用完,换前一块 } cout << ans;
一句话就能说清:两头按段配成一对,这一对的字符不一样,就把这一轮能配成的对数加进答案。 $$\text{ans}=\sum_{\text{两端字符不同的那几轮}} \min(p,q)$$ 区别只在"数谁":婉璐数的是字符,所以得先把字符拼出来;正解数的是段长,一直没离开输入。
这类题我只让婉璐记一句:输入是压缩的,就不要展开。以后看到 \(n\) 段、每段 \(a_i\) 个,先在草稿上估一下 \(\sum a_i\) 有多长,放不进内存就只能在"段"这一层做运算。
知识点:回文对称配对知识点:双指针按段推进易错:展开 \(\sum a_i\)易错:凭空多复制一份串
T3

按位与

and 文件名错,整题归零 0/ 100
给定 \(n\) 个非负整数,从中挑出恰好 \(m\) 个,让这 \(m\) 个数的按位与尽可能大,输出这个最大值。
婉璐写的是"把最大的 m 个数拿出来相与"。排序,ans = a[n],再往前取 \(m-1\) 个数一路 &=。这个想法直觉上说得通,但不是这题的正解。
不过 0 分不是这个贪心造成的,是文件名。第 9 行写的是 ans.out,题面要 and.out。评测机打开 and.out,里面什么都没有,整题不给分——连婉璐能过的那部分点也一起没了。我在平台上把这一处改对重交,45 分。
婉璐的原文(and.cpp 第 7–31 行)
int main(){ freopen("and.in", "r", stdin); freopen("ans.out", "w", stdout); // 第 9 行:题面要 and.out cin>>n>>m; for(int i=1;i<=n;i++) cin>>a[i]; sort(a+1,a+1+n); ans=a[n]; // 先拿最大值 for(int i=n-1;i>=n-m+1;i--) ans&=a[i]; // 再往前取 m-1 个 cout<<ans; }
算法这一层也说清楚。按位与看的是二进制位,不是数值大小。拿三条数据举例:数组 \(8\ 7\ 6\)、\(m=2\),婉璐挑最大的两个 \(8,7\),\(8\&7=0\);而 \(7\&6=6\)。挑最大的几个,在小数据上就站不住。
反例验算:数组 8 7 6,选 2 个
选法二进制按位与结果
8 与 7(婉璐挑的)1000 & 011100
7 与 6(最优)0111 & 011066
正解:从高位往下,问这一位能不能留
int ans = 0; for (int i = 30; i >= 0; i--) { int target = ans | (1 << i); // 试着把第 i 位也点亮 int cnt = 0; for (int j = 1; j <= n; j++) if ((a[j] & target) == target) cnt++; // 能凑齐 target 所有位的数 if (cnt >= m) ans = target; // 至少 m 个 → 这一位留得住 } cout << ans;
这题有两件事要分开办。算法上,按位与的题思路是固定的:从高位到低位逐位判,判据是"能不能凑够 \(m\) 个数同时满足",不是谁数值大。文件名那件事,下一节单独说,那道题让婉璐自己再看一遍。
知识点:按位贪心知识点:可行性计数致命:输出文件名易错:按数值贪心
T4

纸牌游戏

card 方向对,代价算错 48/ 100
点数是 \(1\ldots m\) 的序列,每种点数至少出现一次。从每个起点 \(i=1\ldots n\) 各打一局:桌上没有这个点数就放到最右;桌上有了,就把这个点数连同它右边所有的牌都收回,再把新牌放最右。问每种点数 \(v\) 在这 \(n\) 局结束时还留在桌上的局数。
这题婉璐抓到点子上了。里面用 deque 维护"桌上每种点数只留一张",碰到重复就把左边一路退到这个点数只剩一张为止——这正是题目规则在做的事。这个观察就是正解的出发点,48 分不是白给的,说明小数据上确实跑通了。
代价算错了。外面 \(n\) 个起点,里面再扫一遍到 \(n\),中间每换一个起点还要把 \(3\times10^5\) 的计频数组整个清零一次。就清零这一步,最坏是 \(n\cdot M\approx 5\times10^5\times3\times10^5=1.5\times10^{11}\) 次,再叠上一层 \(n^2\) 的扫描,数据一大这一段就跑不完。48 分来自 \(n\) 小的那些点。
婉璐的原文(card.cpp 第 16–24 行)
for(int k=1;k<=n;k++){ // 每个起点都从头来一遍 q.clear(); memset(f,0,sizeof(f)); // 3×10⁵ 个 int,清 n 次 for(int i=k;i<=n;i++){ if(q.empty()) q.push_back(a[i]); else if(f[a[i]]==0) q.push_back(a[i]); else{ while(!q.empty()){ int t=q.back(); q.pop_back(); f[t]--; if(f[a[i]]==0) break; // 收回该点数及其右侧 } q.push_back(a[i]); } f[a[i]]++; } for(int i=1;i<=m;i++) if(f[i]==1) cnt[i]++; }
正解走的是同一个观察,只是把它用在"所有起点一起算"上。一局打完桌面剩什么,只跟每种点数最后一次出现的位置有关,记作 \(l(u)\)。把 \(l(u)\) 后面那张牌的数值当成父节点 $$p(u)=a_{l(u)+1},\qquad f_u=\{u\}\cup f_{p(u)}$$ 就得到一棵树,\(f_u\) 是从 \(u\) 一路走到根经过的点数。答案于是变成一次子树求和:\(\text{ans}_v=\sum_{u:\,v\in f_u} c_u\),其中 \(c_u\) 是点数 \(u\) 在整个序列里的出现次数。整体 \(O(n+m)\)。
还差的那一步,我上课会直接问。写完"每个起点都来一遍",要接着问一句:这些遍之间有没有一样的地方?婉璐的 deque 已经把"重复会把桌面收回去"算出来了,只是没往"换个起点也能接着用"上想。这一步想到,这题就能往上走,家长最关心的第四题也就是这里。
知识点:滑动窗口知识点:deque 维护正解:按最后出现位置建树 + 子树累加易错:逐步重算不清零
03

一个字母的事

这道题单独拿出来讲:不是能力问题,是流程问题

第二轮只能文件读写,题面给的英文名就是文件名,评测机按这个名字去取答案。婉璐四个文件我逐个看过,另外三个的 freopen 一字不差,只有 and 这份,输出那一行漏了一个字母。

and.in 测试数据 and.cpp 读入 · 计算 · 输出 程序本身是跑通的 第 9 行写错 and.out 空文件 · 0 字节 ans.out 结果全写在这里 评测机 逐点读 and.out → 0 分 评测机不会打开 ans.out

程序读写都没问题,最后一步没写进该写的文件——结果是正确的,位置是错的。

// and.cpp 第 8–9 行 freopen("and.in", "r", stdin); // 对:题面第一个词就是 and freopen("ans.out", "w", stdout); // 错:应为 "and.out"
四道题的读写文件名,逐个核过
题目输入文件输出文件结果
traveltravel.intravel.out正确
stringstring.instring.out正确
andand.inans.out错一个字母
cardcard.incard.out正确
我在平台上重交了一次。把这一处改成 and.out,同一份代码,这题 45 分。所以这一题婉璐不是不会,是没交上去。家长看到 0 分不用紧张,这是流程上的事。
这件事我不想只当"下次注意"讲。从下周开始,每套题交卷前让婉璐固定做三件事:把四个 .out 逐个打开看一眼,空的就是没写;用样例跑一遍,逐字符对;数一遍输出格式对不对。三件事五分钟,这一次就是 45 分。
04

接下来怎么带

本周补动作,两周补模板,赛前把第四题推到底

婉璐的底子不用重搭。这一场 201 分,其中 45 分是流程丢的,剩下三处都差在中间那一步。这三件事算不上新知识,都是做题习惯,我盯两三周能改过来。

本周

把交卷检查做成固定动作

目标:以后不再出现"写对了没交上去"
  • 四道题都练一遍 \(\texttt{freopen}\),题名从题面第一行抄,不凭记忆
  • 交卷前逐个打开 xxx.out:是空的,这题就等于没写
  • 用样例跑 diff;4 位 01 串数一遍位数,多一个空格也算错
验收:随手一套四题,五分钟内把四份输出验完
两周内

补两个模板,加一条"先估规模"

目标:见题先算复杂度,再决定怎么写
  • 压缩输入的段匹配:见到 \(\sum a_i\) 先想能不能只在段上走,string 这题把模板写熟
  • 按位贪心:从高位到低位,判据是够不够 \(m\) 个数同时满足,and 这题照这个重写
  • 先估规模:动手前算一遍 \(n\times n\)、\(n\cdot M\) 这类量级,超过 \(10^{8}\) 就换写法
验收:这两题各手写一遍模板,不看题解能写出来
赛前

把第四题的观察再往前推一步

目标:从"能想到"做到"能落到复杂度"
  • 婉璐已经看到"重复会把局面重置",下一步问:重置之后剩下的东西,对所有起点是不是一样的
  • 专练"每个起点都来一遍"的题:写完先问这些遍能不能共用
  • 大数组不要在循环里 \(\texttt{memset}\),要清就只清用到的部分
验收:能用自己话讲清"为什么一局的结果只跟每种点数最后出现的位置有关"
交卷前自检清单0 / 8
因为结果写进了 ans.out,题面和评测机认的是 and.out。评测机打开 and.out,里面是空的,逐点判错,整题不给分。程序本身没崩。
算,但也没拿到。算,是因为该算的都算了,读入、排序、相与都写了,只是没写进题面要的文件;没拿到,是因为这 45 分本来是手里的,现在掉了。这个数是我在平台上重交测出来的,不是估的。
数"不对称对"这一步没错,问题在婉璐真把 \(\sum a_i\) 个字符拼成了字符串,还多复制了一份倒过来的。总长一大,内存和时限都撑不住,只有总长小的那些点能得分。
算过关。四件事都判了,车厢、排号、同侧、相邻,缺的只是两处条件写错。这题的分是靠读入稳、格式稳拿下来的,不是运气。剩下那 15 分,把过道压成编号就能补回来。
有四题里最大的一块。路径是现成的:把"每个起点重扫一遍"换成"按每种点数最后出现的位置建树,再做一次子树累加"。婉璐的 deque 观察已经是这条路的入口,只是没走到复用那一步。能提到多少要看测试点的分布,这份报告不估分数,我打算赛前带婉璐把这题完整写一遍。
最后说一句。婉璐不属于"题不会做"这一类。读入写得干净、输出格式抓得住、deque 这类工具也用得起来,普及组里这已经是不错的底子。这一场 201 分,45 分输在一个文件名,另外三处差在中间那一步的判断和估算。这三处补上,下次分数会明显不一样。