CSP-J/S复赛考前必须要看的6 句口诀,帮孩子把该拿的分一分不丢
总纲:一保二骗三取舍,四验五稳六装齐
六个关键词:保分、骗分、取舍、验证、稳心、装备
适用范围:CSP-J/S 第二轮(上机)认证。每级 4 道题,每题 100 分,满分 400 分;入门级(J 组)3.5 小时,提高级(S 组)4 小时(以当年官方通知为准)。
写在前面:复赛与初赛的考法完全不同
初赛是笔试,考查知识的掌握情况,错一题只丢这一题的分。
复赛采用 OI 赛制,有三个特点:考试过程中看不到任何评测结果(没有 AC、WA、TLE 等提示);每道题由若干测试点组成,按通过的测试点给分;各测试点独立计分,常见每点 5 分或 10 分,错一个点只丢这一个点的分。
由此可以得出三个结论:
-
没有“全对才得分”。 一道题有 10 个测试点,通过 3 个就是 30 分。拿部分分是复赛最重要的得分手段之一,收益常常不低于多想出一道题的正解。
-
没有评测反馈,正确性只能自己验证。 手推样例、手造小数据是人人都能做的验证方式;对拍是进阶手段,适用条件见板块四④。
-
评测只认文件。 算法再好,只要文件名错、目录错、没写文件读写,或者输出格式与要求不符,整题都是 0 分。每年都有不少考生的失分不在算法上,而在这些与算法无关的环节上。
因此全文分为六个板块,大致按对分数的影响排序:先保证不爆零(保分),再拿足部分分(骗分),然后是题目与时间的安排(取舍)、正确性的验证(验证)、考场心态(稳心),最后是考前需要准备好的代码框架、模板与环境(装备)。
每个板块开头给出一句口诀,口诀串讲中加粗的字(词)各对应板块中的一条内容。考场上想不起细节时,默念口诀即可把整块内容串起来。
板块一 · 保分篇:文界初算,归格不卡
口诀:文界初算,归格不卡
串讲:文件目录先对好,界限空间要开够,初值清零要记牢,算准上界定类型,归零收尾写规范,格式照抄不走样,卡住时限防超时。
本板块是底线。下面 7 条中任何一条出错,都可能导致整题 0 分,与算法水平无关。
① 文 —— 文件读写、文件名与目录结构
复赛必须使用文件输入输出,用 freopen 把标准输入输出重定向到文件。只用 cin、cout 而不写 freopen,整题 0 分。
#include
usingnamespace std;
int main(){
freopen("poker.in","r", stdin);// 从 poker.in 读入,"r" 表示读
freopen("poker.out","w", stdout);// 输出到 poker.out,"w" 表示写
// 下面写正常的读入、计算和输出
return0;
}
目录结构(多数省份为三层结构,具体以本省通知和监考老师的要求为准):
JS-J00123/ ← 总文件夹:用自己的准考证号命名,大小写、连字符照抄
├── poker/ ← 子文件夹:用题目英文名命名,与题面一致(通常全部小写)
│ └── poker.cpp ← 源文件:建议只保留这一个 .cpp
├── explore/
│ └── explore.cpp
├── sticks/
│ └── sticks.cpp
└── chain/
└── chain.cpp
考前逐条核对:
-
两行 freopen 都已写好,且没有被注释掉。本地调试时注释掉、交卷时忘记恢复,是最常见的爆零原因之一;
-
文件名与题面英文名完全一致,大小写、拼写一字不差。Linux 区分大小写,Poker.in 与 poker.in 是两个不同的文件。建议直接从题面复制文件名,不要手打;
-
"r" 对应 stdin,"w" 对应 stdout,不要写反;
-
若考场使用 Windows,先在“文件夹选项”中取消“隐藏已知文件类型的扩展名”,避免把文件存成 poker.cpp.cpp 或 poker.cpp.txt;
-
每个题目文件夹中只保留一个 .cpp,多余的 .in、.out、.exe 和备份文件都删掉;
-
确认代码保存在监考老师指定的盘符和路径下(有的省份使用网页程序回收系统提交)。
建议:建好文件夹后,第一件事就是写好这两行 freopen,不要留到最后再补。本地测试时,把下发的样例输入文件(如 poker1.in)复制一份并改名为 poker.in,运行后查看 poker.out。整个过程都不注释 freopen,也就不存在忘记恢复的问题。
② 界 —— 数组越界、空间超限与爆栈
-
数组大小要分析清楚: 的上限不一定就是数组的上限,要看这个数组在题目中存什么、下标最大会用到多少:
-
-
倍长数组:环形问题常把序列复制一遍接在后面,数组要开到 ;
-
桶数组计数:用 的值作下标时,数组大小由值域决定,而不是由 决定。值域 就要开到 以上;值域达到 时无法直接开桶,需要先离散化;
-
边数组:无向图每条边要存两次,链式前向星的边数组开到 ;
-
负数下标:值可能为负时要加偏移量,数组大小按“最大值 − 最小值”计算;
-
其他结构:二维数组按两维分别分析;字符数组要给 '\0' 留一个位置;线段树一般开 (S 组)。
-
-
分析出真实上限后,再多开少量余量,如需要 时开到 100010。数组大小恰好等于上限,是越界的常见来源(例如下标用到了 n + 1)。
-
下标统一:事先确定下标从 0 开始还是从 1 开始,全程不要混用。循环写成 for (int i = 1; i <= n; i++),却从 a[0] 开始读入,是典型的错位。
-
空间估算:int 占 4 字节,int a[1000000] 约 4 MB。内存限制常见 256 MB 或 512 MB(以题面为准)。二维数组尤其要算清楚:int a[5000][5000] 约 100 MB。
-
大数组定义为全局变量:函数内的局部数组占用栈空间,开得较大时容易爆栈(本机默认栈空间一般只有几 MB)。
-
递归深度:递归深度达到 量级(如在一条链上 DFS)时要注意栈空间。题面或考试说明写明“栈空间限制与内存限制相同”时一般没有问题;没有写明时,可考虑改写为迭代。
-
其他越界:vector、string 下标越界;s.size() - 1 在 s 为空时会得到一个很大的无符号数;memset 的字节数写错。
③ 初 —— 初始化、清零与多组数据重置
-
局部变量不初始化,值是不确定的,属于未定义行为。本机运行结果正确,不代表评测机上也正确。全局变量会自动初始化为 0,局部变量要手动赋初值。
-
多组数据必须重置所有状态:ans、cnt、vis[]、并查集 fa[]、邻接表、vector、map、set、优先队列等,每组开始前都要清空。
-
建议写一个统一的初始化函数,避免遗漏:
// 每组数据开始前调用,清除上一组留下的状态
void init(){
ans =0; cnt =0; tot =0;// 答案、计数器、边的编号清零
for(int i =1; i <= n; i++){
vis[i]=0;// 访问标记清零
head[i]=0;// 链式前向星的表头清零
fa[i]= i;// 并查集:每个点的父亲先设为自己
g[i].clear();// vector 邻接表清空
}
}
-
清空时只清本组用到的部分(如下标 )。数据组数很多时,每组都 memset 整个大数组,可能因此超时。
④ 算 —— 先算出数据上界,再确定变量类型
正确做法不是“一律开 long long”,而是动笔之前先算出每个量在最坏情况下有多大,再按量级选择类型。全部开 long long 不能代替上界估算,还可能带来新的问题。
第一步:从题面的数据范围中找出上界。 不要只看 ,还要看每个元素 、边权、坐标以及答案本身的上界。
第二步:估算答案和中间结果的上界。
|
要存或要算的量 |
典型例子 |
上界 |
类型 |
|
求和 |
,每个 |
long long |
|
|
乘积 |
long long,写成 1LL * a * b |
||
|
组合式 |
时的 n * (n - 1) / 2 |
long long(n * (n - 1) 会先溢出) |
|
|
路径长度累加 |
条边,边权 |
long long |
|
|
计数 cnt++ |
int |
||
|
下标、循环变量 |
int |
第三步:按量级选择类型。
-
上界不超过约 (即 ):int;
-
上界不超过约 (即 ):long long;
-
更大:题目一般会要求取模;否则考虑 __int128 或高精度(__int128 不能直接用 cin、cout 读写)。
一律开 long long 的问题:
-
内存翻倍:int a[10000000] 约 40 MB,改成 long long 约 80 MB,大数组、二维数组可能因此超出内存限制;
-
格式不匹配:用 printf("%d", x) 输出 long long 变量,结果错误;
-
无符号问题无法靠开大类型解决:s.size() - 1 在 s 为空时下溢为很大的正数;unsigned 与 int 混合运算时,int 会被转换为无符号数。
需要重点检查的位置:
-
累加器:sum += a[i],先估算 sum 的最大值,再确定类型;
-
乘法:a * b / c 中先计算 a * b,可能先溢出,应写成 1LL * a * b / c;
-
二分中点:当 l + r 可能超出 int 范围时,写成 mid = l + (r - l) / 2;
-
输出格式与类型一致:long long 用 printf("%lld", x) 或 cout << x;
-
浮点数:用 double,不用 float;比较大小时用 fabs(a - b) < eps(eps 通常取 1e-6 到 1e-9,视精度要求而定),不直接用 ==;
-
无穷大常量:int 用 0x3f3f3f3f(约 ,两个相加不溢出),long long 用 0x3f3f3f3f3f3f3f3f;不要用接近类型上限的值(如 0x7f7f7f7f),两个相加就会溢出。
记住一句话:先算上界,再定类型;遇到求和与乘积,先问一句“最大能到多少”。
⑤ 归 —— 主函数规范与程序正常结束
-
主函数写 int main()。写成 void main() 在 g++ 下会编译错误,整题 0 分。
-
主函数结尾写 return 0;,不要返回非 0 值:return 1; 或 exit(1); 可能被判为运行错误。
-
每组数据都要有输出:不要在某个 if 中提前 return,导致该组数据没有输出。
-
有返回值的函数(非 void 函数),每条执行路径都要有 return,否则属于未定义行为,在 -O2 优化下可能直接运行出错。
⑥ 格 —— 输出格式与要求完全一致
评测时将程序输出与标准答案逐字比较(一般会忽略行末空格和文末换行,以官方说明为准),因此:
-
大小写照抄:要求输出 Yes,写成 YES 或 yes 都判错。固定的输出文字建议直接从题面复制。
-
空格与换行:一行输出几个数、用空格还是换行分隔,逐项对照样例。
-
小数位数:要求保留 2 位就写 printf("%.2f", x),位数不能多也不能少。
-
多组数据:组与组之间是否有空行、是否有前缀,按题面要求输出。
-
问什么答什么:题目要求输出方案却输出了方案数,要求输出最小代价却输出了方案,都会判错。动笔前先在草稿纸上写下“我要输出的是:______”。
-
清除调试输出:调试语句一旦输出到标准输出,该题所有测试点都会判错。调试信息统一用 cerr 输出(输出到标准错误,不进入答案文件);但大量 cerr 输出会拖慢程序,交卷前一并删除。
⑦ 卡 —— 防止超时、死循环与内存超限
-
复杂度估算:一般按每秒约 次简单运算估算,并留出余量;常数较大时还要更保守。时限为 1 秒时可参考:
-
-
: 或 ,如子集枚举、状压;
-
左右:;
-
左右:;
-
:;
-
:。
-
-
死循环的常见原因:while 条件永远成立;在循环体内修改了循环变量;图上搜索没有 vis 标记,导致来回走。
-
递归没有终止条件或没有记忆化:复杂度呈指数增长,表现与死循环相同。
-
输入输出速度:数据量大时,cin、cout 加上 ios::sync_with_stdio(false); cin.tie(0);,或改用 scanf、printf。关闭同步后,不要再把 cin、cout 与 scanf、printf 混用。
-
超时(TLE)、运行错误(RE)与答案错误(WA)一样,该测试点都得 0 分。
板块二 · 骗分篇:暴特表猜,拼分卡时
口诀:暴特表猜,拼分卡时
串讲:暴力先保底,特殊性质别放过,表能打就打,猜也要有依据,拼起各档凑分数,分段处理大小数据,卡住时间兜底输出。
这是复赛中最能拉开差距、也最容易被忽视的板块。 算一笔账:4 道题每题拿 50 分就是 200 分,在不少省份已经是较好的成绩。而每题 50 分往往不需要完整的正解,只需要把下面这些部分分拿到手。
① 暴 —— 暴力保底,先写出最朴素的做法
每道题都要先有一个能得出正确结果的朴素版本。 它有三个作用:
-
直接拿下小数据测试点的分数(通常 10~30 分),这是写暴力的首要目的;
-
正解写错时,它是最后的保障;
-
有余力时,它可以作为检验正解的标准答案,用于对拍(见板块四④)。
常用的暴力手段:
-
全排列枚举:next_permutation,适用于 ;
-
子集枚举:for (int s = 0; s < (1 << n); s++),适用于 ;
-
多重循环枚举所有方案;
-
按题意直接模拟(不少 T1 的正解就是模拟)。
建议:读完题后,先花 10 分钟左右写出暴力,跑通样例并保存,再考虑优化。不要一开始就直接写正解:正解没想出来又没有暴力,这道题就是 0 分。
② 特 —— 特殊性质:针对性地拿分
题面的数据范围表格标明了各档测试点的分布,典型形式如下:
|
测试点编号 |
的范围 |
特殊性质 |
|
1~4 |
无 |
|
|
5~8 |
A |
|
|
9~10 |
B |
每一种特殊性质都可以看作一道独立的小题,难度往往明显低于正解:
-
很小:直接暴力;
-
且图连通(即图是一棵树):可以用树上做法,通常更简单;
-
所有 相等、单调,或只取 0 和 1:往往可以直接推出答案;
-
保证有解或保证某种结构:可以省去相应的判断;
-
、 等退化情形:常常有很简单的做法。
写法:用 if 按数据特征分别处理。
if(n <=10) sol1();// 小数据:暴力枚举
elseif(m == n -1) sol2();// 特殊性质:图是一棵树
elseif(fl) sol3();// 特殊性质:图是一条链(fl 在读入时判断)
else sol4();// 其余数据:正解或近似做法
每个分支都要保证:在自己负责的数据范围内结果正确、不超时。各分支互不影响,分数可以累加。
③ 表 —— 打表与找规律
-
打表:在本机用暴力程序算出小规模的全部答案,把结果写进代码中的数组,提交的程序直接查表输出。适用于答案只与一两个参数有关、规律又难以推出的题目。
-
找规律:数学类、构造类题目,可以先手算或用程序算出前十几项,观察周期、递推关系等规律。例如 2023 年 J 组 T1“小苹果”,先手算若干组小数据,有助于发现每天取走苹果数量的规律。
-
打表要注意源文件大小限制(通常为 100 KB,以考试说明为准)。
-
注意表的下标与题目参数对应正确,不要把第 0 项当成第 1 项。
④ 猜 —— 兜底输出,不留空白
确实没有思路的题,也不要留空。合法的输出仍有可能拿到部分分:
-
输出常量:根据题意输出最可能的结果,如 0、1、-1、n、Yes、No;
-
随机化:随机生成若干合法方案,输出其中最优的一个(对构造题、排列题有时有效);
-
贪心近似:写一个直观上合理的贪心,即使不是最优解,也可能通过部分测试点。
直接输出样例答案,一般只能通过与样例相同的测试点,收益很低,不建议依赖。
前提是输出格式合法,并且程序不会运行错误或超时。 会崩溃的程序得 0 分;能稳定输出的程序才有得分的可能。
按可靠程度排序:暴力 > 特殊性质 > 打表找规律 > 贪心近似 > 猜测输出。猜测输出是最后手段,不要一开始就用。
⑤ 拼 —— 分档拼接:各档分数可以累加
CSP 复赛按测试点独立计分,因此可以把一道题按数据范围看成若干档,每一档写一段代码,各自拿分:
读入数据
├─ 特征 A(如 n ≤ 10) → 做法 1 → 30 分
├─ 特征 B(如图是一条链) → 做法 2 → 20 分
└─ 其余数据 → 做法 3 → 20 分
输出答案
30 + 20 + 20 = 70 分,而并没有写出完整的正解。这是复赛中最实用、效率最高的得分方式之一。
拼接时注意:各档做法共用的数组和全局变量要事先规划好,避免互相干扰。
⑥ 分 —— 数据分治:不同规模用不同做法
同一个问题在不同规模下,适合的做法可能不同,可以都写上:
-
小数据用写法简单、确定正确的做法(如 的 Floyd),大数据用更快的做法(如 Dijkstra);
-
小数据用暴力搜索,大数据用贪心或动态规划;
-
折半枚举(meet-in-the-middle):把 降到约 ,使 左右的数据可以处理;
-
分块:整块预处理,零散部分暴力处理。
关键是准确确定分界线。可以在分界附近手造几组小数据,确认两种做法的结果一致。
⑦ 卡时 —— 用 clock() 保证有输出
对搜索、迭代加深、模拟退火等可以随时停止的算法,可以用 clock() 控制运行时间:快到时限时,立即输出当前找到的最优解。
// 时限 1 秒时,用到 0.85 秒左右就停下,留出输出的时间
while((double)clock()/ CLOCKS_PER_SEC <0.85){
// 继续搜索或迭代,随时更新当前最优答案 best
}
printf("%d\n", best);// 不管有没有搜完,都输出一个合法答案
这样可以把“可能超时得 0 分”变为“一定输出一个合法解,有机会得部分分”。注意:clock() 统计的是程序已经使用的 CPU 时间(包括读入);评测机速度可能与本机不同,要留足余量,不要卡在时限的整数上。
板块三 · 取舍篇:读排保跳,查稳收官
口诀:读排保跳,查稳收官
串讲:先读完四道题,按性价比排顺序,每题保底先落地,卡住就跳不恋战,留出时间查一遍,过了样例手要稳,最后收官照清单。
① 读 —— 先通读四道题(约 15~20 分钟)
不要拿到试卷就开始写第一题。 先用 15~20 分钟读完四道题,包括每道题的数据范围表格(各档部分分都标在这里)。读完给每道题标上:
-
会:思路清晰,预计 80~100 分;
-
半会:有思路但不完整,或只会暴力加部分特殊性质,预计 30~60 分;
-
不会:只能写暴力或猜测输出,预计 5~20 分。
同时估算每道题的性价比:预计得分 ÷ 预计用时,作为排序的依据。
② 排 —— 按性价比排序,不必按题号顺序做
T1~T4 一般按难度递增排列,但每年都有例外,同一道题对不同考生的难度也不同。排序原则:
-
先做确定能拿满分的题(通常是 T1,也可能是自己擅长的某一题);
-
再做部分分多、暴力好写的题;
-
最后做性价比最低的题。
参考时间分配(以提高级 4 小时为例,入门级 3.5 小时按比例缩减):
|
阶段 |
时间 |
做什么 |
|
开局与通读 |
约 25 分钟 |
建好目录与代码框架,读完四题,标难度、定顺序 |
|
第一梯队 |
约 50 分钟 |
拿下最有把握的题,争取满分 |
|
第二梯队 |
约 70 分钟 |
中等题:暴力加部分分 |
|
第三梯队 |
约 65 分钟 |
难题部分分,或尝试正解 |
|
检查与收官 |
约 30 分钟 |
只做检查与小范围修正,不写新的做法 |
③ 保 —— 每道题先保存一个保底版本
每开始一道新题,先写出暴力版本,跑通样例并保存,再在此基础上优化。
-
保底版本写完后另存一份备份(如 poker_bak.cpp,放在题目文件夹以外的位置,交卷前确认它不在提交目录中);
-
优化时保留暴力代码,用 if 分流,不要删掉重写。
④ 跳 —— 设定止损时间,不恋战
-
一道题超过 40 分钟没有实质进展:先换题;
-
一个错误调试超过 20 分钟仍未找到:先放下去做别的题,回头再看,换个思路往往很快就能找到;
-
正解写到一半发现思路有误:立即回到暴力版本,先保住分数。
换题不是放弃,而是重新分配时间。在一道题上卡两个小时、其余三题空白,是复赛中代价最大的失误之一。
⑤ 查 —— 留出专门的检查时间
最后 30 分钟不再写新的做法,只做检查和小范围修正(检查方法见板块四)。这段时间挽回的分数,往往比继续攻一道难题更多。
⑥ 稳 —— 已通过样例的代码,不要随意改动
-
已通过样例的代码,除非确实找到了错误,否则不要改;
-
每次较大的改动之前,先备份原文件;
-
不要为了让代码“更简洁”而重写已经能正确运行的程序。
⑦ 收 —— 收官清单:最后 10 分钟逐条核对
-
四个题目文件夹的名字与题目英文名一致;
-
每个文件夹中只有一个 .cpp,没有多余文件;
-
四份代码都有 freopen,且没有被注释;
-
四份代码都是 int main(),结尾 return 0;;
-
检查所有输出语句,确认没有残留的调试输出;
-
每份代码都已用文件读写方式跑过样例,输出与样例完全一致;
-
代码保存在监考老师指定的位置;
-
使用网页回收系统的省份,确认四道题都已提交成功。
特别提醒:使用网页回收系统时,系统一般不会自动提交,每年都有考生忘记点击提交。结束前 30 分钟先确认一次。
板块四 · 验证篇:验边极拍,扫净方交
口诀:验边极拍,扫净方交
串讲:验证样例要手推,边界数据专门测,极限数据测性能,拍前先看条件够不够(够就对拍,不够就手造小数据),扫读代码找隐患,净除调试残留,方可交卷。
本板块讨论:没有评测反馈时,如何自己确认代码正确。
先说结论:不是每位考生都需要对拍。 对拍(第④条)是有一定门槛的进阶手段:J 组考生时间普遍较紧,不必强求;S 组考生在写完有把握的题目后、仍有充足时间时,可以考虑使用。对多数考生来说,①②③(手推样例、手造边界数据、极限数据测试)加上⑤⑥(静态检查、清理残留),就是完整且够用的验证方案。
① 验 —— 样例不只是“跑一遍”
-
输出与样例一致,不等于程序正确。在草稿纸上手推一遍样例,弄清每一步为什么得到这个结果;
-
用 cerr 输出中间变量(如 cerr << "i=" << i << " dp=" << dp[i] << endl;),核对中间过程;
-
题面中的样例通常较弱,通过样例只能说明大方向没有错,不能说明完全正确;
-
下发的样例文件中常有规模接近上限的大样例,一定要用它测一遍结果和运行时间。
② 边 —— 边界数据:专门检查“差一点”的错误
自己构造下面这些小数据,手算答案后验证:
-
、、(尤其是 ,很多循环和特判会在这里出错);
-
全部元素相同、严格递增、严格递减;
-
取最大值、最小值,含负数、含 0;
-
空串、全部相同的字符;
-
答案恰好等于上界或下界的情况。
边界情况几乎每道题都有对应的测试点。花几分钟构造这类数据,常常能避免丢掉一两个测试点的分数。
③ 极 —— 极限数据:检验运行时间与内存
-
用一个简单的生成程序构造 取上限(如 、)的数据,在本机运行,观察大致耗时;
-
本机耗时接近时限时,就要优化常数或更换算法,因为评测机的速度可能比本机慢;
-
同时观察内存占用,确认不会超出内存限制;
-
构造最坏情况:如让二分次数最多、让树退化成一条链、让哈希大量冲突。
④ 拍 —— 对拍:先判断条件是否满足
对拍是检验正确性最有效的手段之一,但需要投入时间,要根据自己的做题策略决定用不用、给哪道题用。
对拍需要额外准备数据生成器 gen、暴力程序 brute 和一个运行脚本,还要在考场环境中调通,一般需要 20~30 分钟。这段时间如果还能用来多拿一档部分分,就应当先拿分;如果某道题的分数已经是自己成绩的基础,用这段时间守住它就很划算。
一、对某道题对拍,需要同时满足三个条件
-
这道题的正解(或较优做法)已经写完,并通过了样例;
-
这道题能写出暴力,可以作为对照的标准答案;
-
剩余时间充足,并且考前练过对拍的写法。
对拍针对的是单道题,不要求四道题都写完。
二、适合对拍的典型场景
-
S 组的稳分策略:先稳拿前两题,后两题写暴力或部分分,再给前两题对拍,确保这两题不因细节失分,以 200 分以上的总分冲击一等奖。这是很多 S 组考生采用的有效策略;
-
某道题是成绩的基础:代码长、细节多(大模拟、复杂动态规划、图论建图),一旦写错损失很大,值得花时间对拍;
-
提前完成全部题目:其余各题都已拿到能拿的分,剩余时间可以全部用于对拍检查。
三、以下情况不建议对拍
|
情况 |
原因 |
建议做法 |
|
还有题目可以多拿部分分 |
对拍占用的时间,用来多写一档部分分收益更大 |
先拿分,再考虑对拍 |
|
这道题还没写出正解 |
对拍需要正解和暴力两份代码,没有正解就无从对拍 |
先写好暴力,保住分数 |
|
只剩不到 30 分钟 |
生成器、暴力、脚本三样,30 分钟内很难完成,还会打乱节奏 |
静态检查,核对收官清单 |
|
考前没练过对拍 |
考场上第一次写,容易在路径、编译等问题上耽误时间 |
用手造小数据验证(见下) |
一句话:先拿分,再守分。 对拍用来守住已经写完的题,不要在还有分可拿的时候占用时间。
四、不对拍时:用轻量的方法验证
下面四种做法按成本从低到高排列,能发现大部分错误,每种只需几分钟。
1. 手造 3~5 组小数据并手算答案(最推荐)
取 3~6,用纸笔算出答案,再与程序输出对比。这相当于人工对拍,几分钟就能完成,能发现大多数逻辑错误和边界错误。
输入 手算答案 程序输出
n = 1 ? ?
n = 2 ? ?
n = 3,全部相同 ? ?
2. 在程序内用小数据自查(不写脚本的简化对拍)
不写 gen,也不写脚本,只在自己的程序中临时加一段代码:随机生成若干组 的数据,同时调用暴力做法和自己的做法,把两个结果输出到 cerr,逐行比较。
// 临时自查代码,验证完毕后删除
srand(2026);
for(int tc =1; tc <=20; tc++){
n = rand()%8+1;// 随机生成 1~8 个数
for(int i =1; i <= n; i++) a[i]= rand()%10;
cerr <<"n="<< n
<<" brute="<< brute()// 暴力做法的答案
<<" mine="<< solve()<< endl;// 自己做法的答案,两列应当一致
}
注意:brute() 和 solve() 不能修改公共数组,如果会修改,调用前要先恢复。结果一致即可放心;不一致时,把这组数据记下来手算,定位错误。
3. 暴力本身就能得分(见板块二①)
已经写好的暴力版本,在小数据测试点上可以直接得分,不需要对拍也能产生分数。写暴力首先是为了拿分,而不是为了对拍。
4. 静态检查(见下面第⑤条)
通读代码,检查边界、类型、下标和变量名冲突。这种方法没有额外成本,人人都能做。
五、需要对拍时,用最省时的做法
三条经验:
-
brute 不必重写,直接使用早已写好的暴力版本;
-
gen 只生成 左右的小数据,保证暴力能很快算出结果;
-
运行 100~500 组即可,几十秒内没有发现错误,基本可以放心。
各文件的分工:
-
gen.cpp:生成随机小数据,写入 poker.in;
-
brute.cpp:暴力程序,不写 freopen,从标准输入读入,结果重定向到 ans1.out;
-
poker.cpp:自己的正解,保留 freopen,从 poker.in 读入,输出到 poker.out;
-
比较 ans1.out 与 poker.out。
注意两点:第一,正解中保留了 freopen,命令行中的 < 重定向对它不起作用,所以数据要直接写进 poker.in;第二,gen 如果用 srand(time(0)) 设置种子,同一秒内生成的数据完全相同,应由脚本传入不同的种子。对拍请在单独的文件夹中进行,结束后把正解复制回提交目录,不要把 gen、brute 等文件留在提交目录中。
gen.cpp:
#include
usingnamespace std;
int main(int argc,char*argv[]){
srand(atoi(argv[1]));// 用脚本传入的数作为随机种子
int n = rand()%8+1;// n 只取 1~8,保证暴力很快
printf("%d\n", n);
for(int i =1; i <= n; i++) printf("%d ", rand()%10);
printf("\n");
return0;
}
Linux 下的 check.sh:
#!/bin/bash
# 先编译:g++ gen.cpp -o gen;g++ brute.cpp -o brute;g++ poker.cpp -o poker -O2 -std=c++14
# 再执行:chmod +x check.sh,然后运行 ./check.sh
for i in$(seq 1 300);do
./gen$i> poker.in # 第 i 组数据,以 i 为随机种子
./brute< poker.in > ans1.out # 暴力的答案
./poker# 正解:读 poker.in,写 poker.out
ifdiff ans1.out poker.out > /dev/null;then
echo"OK $i"
else
echo"WA on test $i";cp poker.in failed.in;break
fi
done
Windows 下的 check.bat:
@echo off
:loop
gen.exe %random%> poker.in
brute.exe < poker.in > ans1.out
poker.exe
fc ans1.out poker.out > nul
iferrorlevel1(
echo WA
copy poker.in failed.in
pause
gotoend
)
gotoloop
:end
重要前提:这套脚本必须在考前、在与考场相同的环境下练过至少一遍。 考场上第一次写,很容易在执行权限、./ 前缀、路径或编码等问题上卡住,还不如手造几组数据可靠。
⑤ 扫 —— 静态检查代码
交卷前通读一遍代码,重点检查:
-
编译有没有警告(编译时加 -Wall);
-
数组大小是否足够,有没有越界;
-
int 与 long long 有没有混用出错;
-
循环边界是 <= 还是 <,有没有多一或少一;
-
== 有没有误写成 =;
-
变量名有没有与库函数冲突(见下);
-
全局变量与局部变量有没有重名,导致全局变量被屏蔽。
变量名避坑清单:下列名字作为全局变量名时,在 Linux 和 using namespace std; 下可能与库函数或标准库中的名字冲突,导致编译错误(其中一部分在 Windows 下不报错,更容易被忽视):
time、next、prev、index、link、map、pipe、hash、search、ws、y0、y1、yn、j0、j1、jn、pow、min、max、rank、count、left、right。
另外,size、data 在 C++14 下不冲突,但从 C++17 起会冲突,也建议避开。
可以改用 nxt、idx、tim、hs 之类的名字。
⑥ 净 —— 清理干净再交
-
检查所有 cout、printf、puts 语句,删除或注释调试输出(调试请使用 cerr);
-
删除试验用的多余文件,提交目录中只保留需要评测的源文件;
-
确认 freopen 存在且没有被注释;
-
最后再用样例运行一次,确认输出与样例完全一致。
检查完毕、清理干净,才可交卷。
板块五 · 稳心篇:跳满错稳,收存平安
口诀:跳满错稳,收存平安
串讲:卡住就跳不恋战,每题都写满有输出,编译报错先看第一条,心态稳住不慌乱,最后收手不写新代码,存盘提交要确认,平安交卷出考场。
① 跳 —— 卡住就换题,这是策略而不是逃避
具体做法见板块三④。从心态上再强调一次:在一道题上卡 90 分钟,是复赛中最常见、代价也最大的失误之一。 换一道题做,回头再看时往往思路就清楚了。
② 满 —— 每道题都要有代码、有输出
任何一道题都不应留空。 即使只会输出 -1,也要写好并保存:
-
空白必然是 0 分;
-
有输出就有得分的可能;
-
有些测试点本身比较容易(如“无解时输出 -1”的情形)。
③ 错 —— 编译错误(CE)的处理
-
先看第一条错误。C++ 的一处错误常常引出一连串报错,改完第一条再重新编译;
-
常见原因:缺分号或括号;使用 freopen 却没有包含 (使用万能头文件 可以避免);变量名与库函数冲突;使用了 C++14 中已移除的 gets(可改用 scanf("%s", s) 或 cin);混入中文标点;函数未声明就调用;类型不匹配;
-
不使用不熟悉的语法。考场代码以稳为先,在 -O2 -std=c++14 的编译环境下,写法越朴素越好。
④ 稳 —— 慌乱时先做有把握的题
-
卡住、思路中断时:停下来 30 秒,喝口水,去做一道确定能拿分的题,找回节奏;
-
不要和别人比进度。考场上看不到任何人的分数,别人看似在写正解,也可能在调文件读写;
-
遇到电脑死机、环境异常:立即举手报告监考老师,不要自行重启或处理。
⑤ 收 —— 最后 10 分钟不写新代码
这段时间写新代码,风险远大于收益:来不及验证,可能引入新的错误,甚至来不及保存。最后 10 分钟只核对收官清单、确认提交。(最后 30 分钟起就不再写新的做法,见板块三⑤。)
⑥ 存 —— 随时保存,确认提交位置
-
写代码过程中随时按 Ctrl+S 保存,养成写几行就保存一次的习惯;
-
每做完一题,确认一次文件的位置和名字;
-
使用网页回收系统的省份,结束前 30 分钟确认一次提交成功。
文件存对了、提交成功了,才算平安交卷。
板块六 · 装备篇:快文模卡,调试不慌
口诀:快文模卡,调试不慌
串讲:快读快写加万能头,文件框架写熟练,模板代码记在手,卡时兜底备一份,调试先看中间量,遇到问题不慌乱。
本板块是考前需要准备好的工具:代码框架、算法模板、调试方法,以及对考试环境的熟悉程度。它们本身不直接产生分数,但决定了考场上能否把想清楚的做法稳定地写出来。前五个板块讲“想什么、做什么”,本板块讲“靠什么做”。
① 快 —— 万能头文件与输入输出加速
#include // 万能头文件,包含常用的全部头文件
usingnamespace std;
int main(){
freopen("xxx.in","r", stdin);
freopen("xxx.out","w", stdout);
ios::sync_with_stdio(false);// 关闭与 scanf/printf 的同步,加快 cin/cout
cin.tie(0);
// 注意:关闭同步后,不要再混用 scanf/printf 与 cin/cout
return0;
}
编译选项通常为 -O2 -std=c++14(以当年官方公告为准)。在这一标准下不要使用 C++17 及以上的特性,如结构化绑定、std::gcd,否则会编译错误。需要求最大公约数时,可以自己写辗转相除法,或使用 __gcd。
② 文 —— 文件框架练到熟练(对应板块一①)
把上面的框架在考前反复默写,并固定成一套动作:
建三层文件夹 → 新建 xxx.cpp → 写框架(万能头文件和两行 freopen)→ 复制样例为 xxx.in → 编译运行 → 查看 xxx.out。
这套动作应在 2 分钟内完成,不占用思考时间,把精力全部留给题目本身。
③ 模 —— 常用模板清单(考前逐条确认能独立写出)
-
基础:排序 sort、二分(lower_bound 或手写)、前缀和、差分、离散化、双指针、单调栈、单调队列;
-
搜索:DFS、BFS、记忆化搜索、剪枝、迭代加深;
-
图论:邻接表建图、并查集(路径压缩)、Dijkstra(堆优化)、Kruskal 最小生成树、拓扑排序、树上 DFS、LCA(S 组);
-
动态规划:线性 DP、背包(01 背包、完全背包、多重背包)、区间 DP、树形 DP、状压 DP(S 组)、最长上升子序列;
-
数据结构:树状数组、线段树(S 组)、ST 表、堆与优先队列;
-
数学:最大公约数与最小公倍数、素数筛、快速幂、组合数、高精度(J 组偶尔用到);
-
字符串:string 的常用操作(substr、find 等)、KMP(S 组)。
考前最后一周不建议再做大量新题,把这份清单逐条默写一遍。考场上想不起模板的写法,是很可惜的失分。
④ 卡 —— 卡时模板(对应板块二⑦)
需要时直接嵌入搜索或迭代过程,保证在时限内输出一个合法答案:
double st = clock();// 全局变量:记录程序开始的时刻
bool tout(){// 判断是否快到时限(此处按 1 秒时限计)
return(clock()- st)/ CLOCKS_PER_SEC >0.85;
}
// 用法:在搜索函数开头写 if (tout()) { 输出 best; exit(0); }
⑤ 调 —— 输出中间过程,定位错误
考场上最可靠的调试方法,就是把程序运行的中间过程输出出来,与手算结果逐项对照,找到第一处不一致的地方。
1. 用 cerr 输出,并且带上标签
调试信息用 cerr 输出,不会进入答案文件。每个输出都写清楚是哪个变量、第几轮,方便对照:
cerr <<"i="<< i <<" sum="<< sum << endl;// 写明变量名,不要只输出一串数
2. 定位错误的步骤
-
先找到出错的小数据:用手造的小数据( 取 3~6)找到程序答案与手算答案不一致的一组,后面都用这组数据调试;
-
从前往后查,找第一处不一致:先确认读入是否正确,再在程序中间输出关键结果,判断错误出在前半段还是后半段,逐步缩小范围;
-
对照手算过程:例如动态规划,先在草稿纸上算出 dp 表的前几项,再与程序输出逐项比较,第一处不同的位置就是转移出错的地方;
-
程序运行出错(RE)时:在程序几个位置分别输出“到达位置 1”“到达位置 2”,看最后一条输出出现在哪里,崩溃点就在它之后,再重点检查那里的数组下标。cerr 不经过缓冲,程序崩溃前的输出也能看到,这一点上比 cout 可靠;
-
超时(TLE)时:输出循环执行的次数或递归调用的次数,确认是否与复杂度估算相符,判断是哪一段代码执行次数过多。
3. 哪些数据最值得输出
-
读入后的数据:确认读入正确,尤其是多组数据、字符串和边的读入;
-
递推或动态规划:每一轮的状态,如 dp[i];二维的 dp 表可以按行输出;
-
搜索与递归:进入函数时的参数(当前位置、深度、当前的和等),以及什么时候返回;
-
图论:建好的边(逐点输出邻接表),以及 BFS、最短路结束后的 dis 数组;
-
模拟:每一步操作之后的关键状态;
-
二分:每一轮的 l、r、mid 以及判断函数的结果;
-
排序或贪心:排序之后的数组,以及每一步选择了哪一个;
-
边界附近:循环开始和结束的几轮,如 i = 1、i = n 时的值。
数据较多时,只输出前几轮或满足条件的部分,避免输出太多看不过来:
for(int i =1; i <= n; i++){
dp[i]= max(dp[i -1], dp[i -2]+ a[i]);// 状态转移
if(i <=5) cerr <<"i="<< i <<" dp="<< dp[i]<< endl;// 只看前 5 轮,与手算对照
}
调试结束后删除所有 cerr 语句,交卷前再检查一遍(见板块四⑥)。
⑥ 不慌 —— 考前熟悉环境,考场才不慌
-
提前了解本省的考试系统、编程环境和操作方式(不少省份使用 NOI Linux 2.0,也有省份使用 Windows),参加过本省模拟测试或考前试机的,按当时的操作练熟即可;
-
重点熟悉几件事:用什么编辑器写代码、怎样新建文件夹和源文件、怎样编译运行、代码保存在哪里、怎样提交;
-
考前一天不熬夜,检查好证件,记牢自己的准考证号。J 组和 S 组的准考证号不同,两场都参加的考生要分别核对。
附录:考前 24 小时与考场当天
考前一天
-
默写一遍文件框架(freopen 与 return 0);
-
默写常用模板清单,写不出的再练一遍;
-
做 1~2 道简单的模拟题保持手感,不做大量新题,不熬夜;
-
准备好准考证和证件,记牢自己的准考证号;
-
把本文的六句口诀抄在纸上,入场前再看一遍(考场一般不允许携带纸质资料,入场前要收好)。
考场时间安排(以提高级 4 小时为例)
|
时间 |
做什么 |
|
第 0~5 分钟 |
建好三层文件夹,创建四个 .cpp,写好框架和 freopen |
|
第 5~25 分钟 |
通读四题,标难度,定做题顺序 |
|
第 25~75 分钟 |
第一梯队:拿下最有把握的题,争取满分 |
|
第 75~145 分钟 |
第二梯队:中等题,暴力、特殊性质加部分优化 |
|
第 145~210 分钟 |
第三梯队:难题部分分或尝试正解,中途手造几组小数据验证一次 |
|
第 210~230 分钟 |
停止写新的做法,按板块四检查各题 |
|
第 230~240 分钟 |
按收官清单逐条核对,确认四题都已保存或提交 |
手造小数据验证是基本动作。采用“先稳拿前两题、后两题拿部分分”策略的考生,可以在后两题的部分分写完后,把剩余时间用于给前两题对拍。
考前一分钟,默念六句口诀
一保二骗三取舍,四验五稳六装齐
保分:文界初算,归格不卡
骗分:暴特表猜,拼分卡时
取舍:读排保跳,查稳收官
验证:验边极拍,扫净方交
稳心:跳满错稳,收存平安
装备:快文模卡,调试不慌
最后: 复赛拿高分的考生,往往不是算法最强的,而是不爆零、拿足部分分、时间安排合理的。把这六句口诀记熟,就能在细节上少丢分。
祝各位考生:文件全对,样例全过,暴力都得分,正解都想通。
免责声明
本文由新东方优编程教练团结合 CSP-J/S 第二轮公开考试规则、历年公开真题及日常教学与集训经验整理,内容为考前复习方法与考场策略建议,仅供备考参考,不构成对考试成绩的承诺。考试时长、编译选项、考生目录结构、文件命名、提交方式及评测规则等具体要求,各省可能存在差异,且可能逐年调整,均以 CCF 官方及本省认证点发布的考试通知、考场说明和监考老师的要求为准。文中的时间分配、复杂度估算与代码示例为通用参考,考生应结合自身水平和具体题目灵活调整。如遇争议,以 CCF 官方解释为准。
新东方优编程教练团 · 2026 年度教研出品
免责声明:以上内容来源于互联网,如有侵权请联系我们删除。