CSP-J/S复赛考前必须要看的6 句口诀,帮孩子把该拿的分一分不丢

栏目:升学资讯 来源:网络 阅读:7
  新东方优编程教练团 · 年度教研出品

  总纲:一保二骗三取舍,四验五稳六装齐

  六个关键词:保分、骗分、取舍、验证、稳心、装备

  适用范围:CSP-J/S 第二轮(上机)认证。每级 4 道题,每题 100 分,满分 400 分;入门级(J 组)3.5 小时,提高级(S 组)4 小时(以当年官方通知为准)。

  写在前面:复赛与初赛的考法完全不同

  初赛是笔试,考查知识的掌握情况,错一题只丢这一题的分。

  复赛采用 OI 赛制,有三个特点:考试过程中看不到任何评测结果(没有 AC、WA、TLE 等提示);每道题由若干测试点组成,按通过的测试点给分;各测试点独立计分,常见每点 5 分或 10 分,错一个点只丢这一个点的分。

  由此可以得出三个结论:

  1.   没有“全对才得分”。 一道题有 10 个测试点,通过 3 个就是 30 分。拿部分分是复赛最重要的得分手段之一,收益常常不低于多想出一道题的正解。

  2.   没有评测反馈,正确性只能自己验证。 手推样例、手造小数据是人人都能做的验证方式;对拍是进阶手段,适用条件见板块四④。

  3.   评测只认文件。 算法再好,只要文件名错、目录错、没写文件读写,或者输出格式与要求不符,整题都是 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

  考前逐条核对:

  1.   两行 freopen 都已写好,且没有被注释掉。本地调试时注释掉、交卷时忘记恢复,是最常见的爆零原因之一;

  2.   文件名与题面英文名完全一致,大小写、拼写一字不差。Linux 区分大小写,Poker.in 与 poker.in 是两个不同的文件。建议直接从题面复制文件名,不要手打;

  3.   "r" 对应 stdin,"w" 对应 stdout,不要写反;

  4.   若考场使用 Windows,先在“文件夹选项”中取消“隐藏已知文件类型的扩展名”,避免把文件存成 poker.cpp.cpp 或 poker.cpp.txt;

  5.   每个题目文件夹中只保留一个 .cpp,多余的 .in、.out、.exe 和备份文件都删掉;

  6.   确认代码保存在监考老师指定的盘符和路径下(有的省份使用网页程序回收系统提交)。

  建议:建好文件夹后,第一件事就是写好这两行 freopen,不要留到最后再补。本地测试时,把下发的样例输入文件(如 poker1.in)复制一份并改名为 poker.in,运行后查看 poker.out。整个过程都不注释 freopen,也就不存在忘记恢复的问题。

  ② 界 —— 数组越界、空间超限与爆栈

  1.   数组大小要分析清楚: 的上限不一定就是数组的上限,要看这个数组在题目中存什么、下标最大会用到多少:

    1.   倍长数组:环形问题常把序列复制一遍接在后面,数组要开到 ;

    2.   桶数组计数:用 的值作下标时,数组大小由值域决定,而不是由 决定。值域 就要开到 以上;值域达到 时无法直接开桶,需要先离散化;

    3.   边数组:无向图每条边要存两次,链式前向星的边数组开到 ;

    4.   负数下标:值可能为负时要加偏移量,数组大小按“最大值 − 最小值”计算;

    5.   其他结构:二维数组按两维分别分析;字符数组要给 '\0' 留一个位置;线段树一般开 (S 组)。

  •   分析出真实上限后,再多开少量余量,如需要 时开到 100010。数组大小恰好等于上限,是越界的常见来源(例如下标用到了 n + 1)。

  1.   下标统一:事先确定下标从 0 开始还是从 1 开始,全程不要混用。循环写成 for (int i = 1; i <= n; i++),却从 a[0] 开始读入,是典型的错位。

  2.   空间估算:int 占 4 字节,int a[1000000] 约 4 MB。内存限制常见 256 MB 或 512 MB(以题面为准)。二维数组尤其要算清楚:int a[5000][5000] 约 100 MB。

  3.   大数组定义为全局变量:函数内的局部数组占用栈空间,开得较大时容易爆栈(本机默认栈空间一般只有几 MB)。

  4.   递归深度:递归深度达到 量级(如在一条链上 DFS)时要注意栈空间。题面或考试说明写明“栈空间限制与内存限制相同”时一般没有问题;没有写明时,可考虑改写为迭代。

  5.   其他越界:vector、string 下标越界;s.size() - 1 在 s 为空时会得到一个很大的无符号数;memset 的字节数写错。

  ③ 初 —— 初始化、清零与多组数据重置

  1.   局部变量不初始化,值是不确定的,属于未定义行为。本机运行结果正确,不代表评测机上也正确。全局变量会自动初始化为 0,局部变量要手动赋初值。

  2.   多组数据必须重置所有状态:ans、cnt、vis[]、并查集 fa[]、邻接表、vector、map、set、优先队列等,每组开始前都要清空。

  3.   建议写一个统一的初始化函数,避免遗漏:

  // 每组数据开始前调用,清除上一组留下的状态

  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 邻接表清空

  }

  }

  1.   清空时只清本组用到的部分(如下标 )。数据组数很多时,每组都 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

  第三步:按量级选择类型。

  1.   上界不超过约 (即 ):int;

  2.   上界不超过约 (即 ):long long;

  3.   更大:题目一般会要求取模;否则考虑 __int128 或高精度(__int128 不能直接用 cin、cout 读写)。

  一律开 long long 的问题:

  1.   内存翻倍:int a[10000000] 约 40 MB,改成 long long 约 80 MB,大数组、二维数组可能因此超出内存限制;

  2.   格式不匹配:用 printf("%d", x) 输出 long long 变量,结果错误;

  3.   无符号问题无法靠开大类型解决:s.size() - 1 在 s 为空时下溢为很大的正数;unsigned 与 int 混合运算时,int 会被转换为无符号数。

  需要重点检查的位置:

  1.   累加器:sum += a[i],先估算 sum 的最大值,再确定类型;

  2.   乘法:a * b / c 中先计算 a * b,可能先溢出,应写成 1LL * a * b / c;

  3.   二分中点:当 l + r 可能超出 int 范围时,写成 mid = l + (r - l) / 2;

  4.   输出格式与类型一致:long long 用 printf("%lld", x) 或 cout << x;

  5.   浮点数:用 double,不用 float;比较大小时用 fabs(a - b) < eps(eps 通常取 1e-6 到 1e-9,视精度要求而定),不直接用 ==;

  6.   无穷大常量:int 用 0x3f3f3f3f(约 ,两个相加不溢出),long long 用 0x3f3f3f3f3f3f3f3f;不要用接近类型上限的值(如 0x7f7f7f7f),两个相加就会溢出。

  记住一句话:先算上界,再定类型;遇到求和与乘积,先问一句“最大能到多少”。

  ⑤ 归 —— 主函数规范与程序正常结束

  1.   主函数写 int main()。写成 void main() 在 g++ 下会编译错误,整题 0 分。

  2.   主函数结尾写 return 0;,不要返回非 0 值:return 1; 或 exit(1); 可能被判为运行错误。

  3.   每组数据都要有输出:不要在某个 if 中提前 return,导致该组数据没有输出。

  4.   有返回值的函数(非 void 函数),每条执行路径都要有 return,否则属于未定义行为,在 -O2 优化下可能直接运行出错。

  ⑥ 格 —— 输出格式与要求完全一致

  评测时将程序输出与标准答案逐字比较(一般会忽略行末空格和文末换行,以官方说明为准),因此:

  1.   大小写照抄:要求输出 Yes,写成 YES 或 yes 都判错。固定的输出文字建议直接从题面复制。

  2.   空格与换行:一行输出几个数、用空格还是换行分隔,逐项对照样例。

  3.   小数位数:要求保留 2 位就写 printf("%.2f", x),位数不能多也不能少。

  4.   多组数据:组与组之间是否有空行、是否有前缀,按题面要求输出。

  5.   问什么答什么:题目要求输出方案却输出了方案数,要求输出最小代价却输出了方案,都会判错。动笔前先在草稿纸上写下“我要输出的是:______”。

  6.   清除调试输出:调试语句一旦输出到标准输出,该题所有测试点都会判错。调试信息统一用 cerr 输出(输出到标准错误,不进入答案文件);但大量 cerr 输出会拖慢程序,交卷前一并删除。

  ⑦ 卡 —— 防止超时、死循环与内存超限

  1.   复杂度估算:一般按每秒约 次简单运算估算,并留出余量;常数较大时还要更保守。时限为 1 秒时可参考:

    1.   : 或 ,如子集枚举、状压;

    2.   左右:;

    3.   左右:;

    4.   :;

    5.   :。

  2.   死循环的常见原因:while 条件永远成立;在循环体内修改了循环变量;图上搜索没有 vis 标记,导致来回走。

  3.   递归没有终止条件或没有记忆化:复杂度呈指数增长,表现与死循环相同。

  4.   输入输出速度:数据量大时,cin、cout 加上 ios::sync_with_stdio(false); cin.tie(0);,或改用 scanf、printf。关闭同步后,不要再把 cin、cout 与 scanf、printf 混用。

  5.   超时(TLE)、运行错误(RE)与答案错误(WA)一样,该测试点都得 0 分。

  板块二 · 骗分篇:暴特表猜,拼分卡时

  口诀:暴特表猜,拼分卡时

  串讲:暴力先保底,特殊性质别放过,表能打就打,猜也要有依据,拼起各档凑分数,分段处理大小数据,卡住时间兜底输出。

  这是复赛中最能拉开差距、也最容易被忽视的板块。 算一笔账:4 道题每题拿 50 分就是 200 分,在不少省份已经是较好的成绩。而每题 50 分往往不需要完整的正解,只需要把下面这些部分分拿到手。

  ① 暴 —— 暴力保底,先写出最朴素的做法

  每道题都要先有一个能得出正确结果的朴素版本。 它有三个作用:

  1.   直接拿下小数据测试点的分数(通常 10~30 分),这是写暴力的首要目的;

  2.   正解写错时,它是最后的保障;

  3.   有余力时,它可以作为检验正解的标准答案,用于对拍(见板块四④)。

  常用的暴力手段:

  1.   全排列枚举:next_permutation,适用于 ;

  2.   子集枚举:for (int s = 0; s < (1 << n); s++),适用于 ;

  3.   多重循环枚举所有方案;

  4.   按题意直接模拟(不少 T1 的正解就是模拟)。

  建议:读完题后,先花 10 分钟左右写出暴力,跑通样例并保存,再考虑优化。不要一开始就直接写正解:正解没想出来又没有暴力,这道题就是 0 分。

  ② 特 —— 特殊性质:针对性地拿分

  题面的数据范围表格标明了各档测试点的分布,典型形式如下:

  测试点编号

  的范围

  特殊性质

  1~4

  无

  5~8

  A

  9~10

  B

  每一种特殊性质都可以看作一道独立的小题,难度往往明显低于正解:

  1.   很小:直接暴力;

  2.   且图连通(即图是一棵树):可以用树上做法,通常更简单;

  3.   所有 相等、单调,或只取 0 和 1:往往可以直接推出答案;

  4.   保证有解或保证某种结构:可以省去相应的判断;

  5.   、 等退化情形:常常有很简单的做法。

  写法:用 if 按数据特征分别处理。

  if(n <=10) sol1();// 小数据:暴力枚举

  elseif(m == n -1) sol2();// 特殊性质:图是一棵树

  elseif(fl) sol3();// 特殊性质:图是一条链(fl 在读入时判断)

  else sol4();// 其余数据:正解或近似做法

  每个分支都要保证:在自己负责的数据范围内结果正确、不超时。各分支互不影响,分数可以累加。

  ③ 表 —— 打表与找规律

  1.   打表:在本机用暴力程序算出小规模的全部答案,把结果写进代码中的数组,提交的程序直接查表输出。适用于答案只与一两个参数有关、规律又难以推出的题目。

  2.   找规律:数学类、构造类题目,可以先手算或用程序算出前十几项,观察周期、递推关系等规律。例如 2023 年 J 组 T1“小苹果”,先手算若干组小数据,有助于发现每天取走苹果数量的规律。

  3.   打表要注意源文件大小限制(通常为 100 KB,以考试说明为准)。

  4.   注意表的下标与题目参数对应正确,不要把第 0 项当成第 1 项。

  ④ 猜 —— 兜底输出,不留空白

  确实没有思路的题,也不要留空。合法的输出仍有可能拿到部分分:

  1.   输出常量:根据题意输出最可能的结果,如 0、1、-1、n、Yes、No;

  2.   随机化:随机生成若干合法方案,输出其中最优的一个(对构造题、排列题有时有效);

  3.   贪心近似:写一个直观上合理的贪心,即使不是最优解,也可能通过部分测试点。

  直接输出样例答案,一般只能通过与样例相同的测试点,收益很低,不建议依赖。

  前提是输出格式合法,并且程序不会运行错误或超时。 会崩溃的程序得 0 分;能稳定输出的程序才有得分的可能。

  按可靠程度排序:暴力 > 特殊性质 > 打表找规律 > 贪心近似 > 猜测输出。猜测输出是最后手段,不要一开始就用。

  ⑤ 拼 —— 分档拼接:各档分数可以累加

  CSP 复赛按测试点独立计分,因此可以把一道题按数据范围看成若干档,每一档写一段代码,各自拿分:

  读入数据

  ├─ 特征 A(如 n ≤ 10) → 做法 1 → 30 分

  ├─ 特征 B(如图是一条链) → 做法 2 → 20 分

  └─ 其余数据 → 做法 3 → 20 分

  输出答案

  30 + 20 + 20 = 70 分,而并没有写出完整的正解。这是复赛中最实用、效率最高的得分方式之一。

  拼接时注意:各档做法共用的数组和全局变量要事先规划好,避免互相干扰。

  ⑥ 分 —— 数据分治:不同规模用不同做法

  同一个问题在不同规模下,适合的做法可能不同,可以都写上:

  1.   小数据用写法简单、确定正确的做法(如 的 Floyd),大数据用更快的做法(如 Dijkstra);

  2.   小数据用暴力搜索,大数据用贪心或动态规划;

  3.   折半枚举(meet-in-the-middle):把 降到约 ,使 左右的数据可以处理;

  4.   分块:整块预处理,零散部分暴力处理。

  关键是准确确定分界线。可以在分界附近手造几组小数据,确认两种做法的结果一致。

  ⑦ 卡时 —— 用 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 分钟读完四道题,包括每道题的数据范围表格(各档部分分都标在这里)。读完给每道题标上:

  1.   会:思路清晰,预计 80~100 分;

  2.   半会:有思路但不完整,或只会暴力加部分特殊性质,预计 30~60 分;

  3.   不会:只能写暴力或猜测输出,预计 5~20 分。

  同时估算每道题的性价比:预计得分 ÷ 预计用时,作为排序的依据。

  ② 排 —— 按性价比排序,不必按题号顺序做

  T1~T4 一般按难度递增排列,但每年都有例外,同一道题对不同考生的难度也不同。排序原则:

  1.   先做确定能拿满分的题(通常是 T1,也可能是自己擅长的某一题);

  2.   再做部分分多、暴力好写的题;

  3.   最后做性价比最低的题。

  参考时间分配(以提高级 4 小时为例,入门级 3.5 小时按比例缩减):

  阶段

  时间

  做什么

  开局与通读

  约 25 分钟

  建好目录与代码框架,读完四题,标难度、定顺序

  第一梯队

  约 50 分钟

  拿下最有把握的题,争取满分

  第二梯队

  约 70 分钟

  中等题:暴力加部分分

  第三梯队

  约 65 分钟

  难题部分分,或尝试正解

  检查与收官

  约 30 分钟

  只做检查与小范围修正,不写新的做法

  ③ 保 —— 每道题先保存一个保底版本

  每开始一道新题,先写出暴力版本,跑通样例并保存,再在此基础上优化。

  1.   保底版本写完后另存一份备份(如 poker_bak.cpp,放在题目文件夹以外的位置,交卷前确认它不在提交目录中);

  2.   优化时保留暴力代码,用 if 分流,不要删掉重写。

  ④ 跳 —— 设定止损时间,不恋战

  1.   一道题超过 40 分钟没有实质进展:先换题;

  2.   一个错误调试超过 20 分钟仍未找到:先放下去做别的题,回头再看,换个思路往往很快就能找到;

  3.   正解写到一半发现思路有误:立即回到暴力版本,先保住分数。

  换题不是放弃,而是重新分配时间。在一道题上卡两个小时、其余三题空白,是复赛中代价最大的失误之一。

  ⑤ 查 —— 留出专门的检查时间

  最后 30 分钟不再写新的做法,只做检查和小范围修正(检查方法见板块四)。这段时间挽回的分数,往往比继续攻一道难题更多。

  ⑥ 稳 —— 已通过样例的代码,不要随意改动

  1.   已通过样例的代码,除非确实找到了错误,否则不要改;

  2.   每次较大的改动之前,先备份原文件;

  3.   不要为了让代码“更简洁”而重写已经能正确运行的程序。

  ⑦ 收 —— 收官清单:最后 10 分钟逐条核对

  1.   四个题目文件夹的名字与题目英文名一致;

  2.   每个文件夹中只有一个 .cpp,没有多余文件;

  3.   四份代码都有 freopen,且没有被注释;

  4.   四份代码都是 int main(),结尾 return 0;;

  5.   检查所有输出语句,确认没有残留的调试输出;

  6.   每份代码都已用文件读写方式跑过样例,输出与样例完全一致;

  7.   代码保存在监考老师指定的位置;

  8.   使用网页回收系统的省份,确认四道题都已提交成功。

  特别提醒:使用网页回收系统时,系统一般不会自动提交,每年都有考生忘记点击提交。结束前 30 分钟先确认一次。

  板块四 · 验证篇:验边极拍,扫净方交

  口诀:验边极拍,扫净方交

  串讲:验证样例要手推,边界数据专门测,极限数据测性能,拍前先看条件够不够(够就对拍,不够就手造小数据),扫读代码找隐患,净除调试残留,方可交卷。

  本板块讨论:没有评测反馈时,如何自己确认代码正确。

  先说结论:不是每位考生都需要对拍。 对拍(第④条)是有一定门槛的进阶手段:J 组考生时间普遍较紧,不必强求;S 组考生在写完有把握的题目后、仍有充足时间时,可以考虑使用。对多数考生来说,①②③(手推样例、手造边界数据、极限数据测试)加上⑤⑥(静态检查、清理残留),就是完整且够用的验证方案。

  ① 验 —— 样例不只是“跑一遍”

  1.   输出与样例一致,不等于程序正确。在草稿纸上手推一遍样例,弄清每一步为什么得到这个结果;

  2.   用 cerr 输出中间变量(如 cerr << "i=" << i << " dp=" << dp[i] << endl;),核对中间过程;

  3.   题面中的样例通常较弱,通过样例只能说明大方向没有错,不能说明完全正确;

  4.   下发的样例文件中常有规模接近上限的大样例,一定要用它测一遍结果和运行时间。

  ② 边 —— 边界数据:专门检查“差一点”的错误

  自己构造下面这些小数据,手算答案后验证:

  1.   、、(尤其是 ,很多循环和特判会在这里出错);

  2.   全部元素相同、严格递增、严格递减;

  3.   取最大值、最小值,含负数、含 0;

  4.   空串、全部相同的字符;

  5.   答案恰好等于上界或下界的情况。

  边界情况几乎每道题都有对应的测试点。花几分钟构造这类数据,常常能避免丢掉一两个测试点的分数。

  ③ 极 —— 极限数据:检验运行时间与内存

  1.   用一个简单的生成程序构造 取上限(如 、)的数据,在本机运行,观察大致耗时;

  2.   本机耗时接近时限时,就要优化常数或更换算法,因为评测机的速度可能比本机慢;

  3.   同时观察内存占用,确认不会超出内存限制;

  4.   构造最坏情况:如让二分次数最多、让树退化成一条链、让哈希大量冲突。

  ④ 拍 —— 对拍:先判断条件是否满足

  对拍是检验正确性最有效的手段之一,但需要投入时间,要根据自己的做题策略决定用不用、给哪道题用。

  对拍需要额外准备数据生成器 gen、暴力程序 brute 和一个运行脚本,还要在考场环境中调通,一般需要 20~30 分钟。这段时间如果还能用来多拿一档部分分,就应当先拿分;如果某道题的分数已经是自己成绩的基础,用这段时间守住它就很划算。

  一、对某道题对拍,需要同时满足三个条件

  1.   这道题的正解(或较优做法)已经写完,并通过了样例;

  2.   这道题能写出暴力,可以作为对照的标准答案;

  3.   剩余时间充足,并且考前练过对拍的写法。

  对拍针对的是单道题,不要求四道题都写完。

  二、适合对拍的典型场景

  1.   S 组的稳分策略:先稳拿前两题,后两题写暴力或部分分,再给前两题对拍,确保这两题不因细节失分,以 200 分以上的总分冲击一等奖。这是很多 S 组考生采用的有效策略;

  2.   某道题是成绩的基础:代码长、细节多(大模拟、复杂动态规划、图论建图),一旦写错损失很大,值得花时间对拍;

  3.   提前完成全部题目:其余各题都已拿到能拿的分,剩余时间可以全部用于对拍检查。

  三、以下情况不建议对拍

  情况

  原因

  建议做法

  还有题目可以多拿部分分

  对拍占用的时间,用来多写一档部分分收益更大

  先拿分,再考虑对拍

  这道题还没写出正解

  对拍需要正解和暴力两份代码,没有正解就无从对拍

  先写好暴力,保住分数

  只剩不到 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. 静态检查(见下面第⑤条)

  通读代码,检查边界、类型、下标和变量名冲突。这种方法没有额外成本,人人都能做。

  五、需要对拍时,用最省时的做法

  三条经验:

  1.   brute 不必重写,直接使用早已写好的暴力版本;

  2.   gen 只生成 左右的小数据,保证暴力能很快算出结果;

  3.   运行 100~500 组即可,几十秒内没有发现错误,基本可以放心。

  各文件的分工:

  1.   gen.cpp:生成随机小数据,写入 poker.in;

  2.   brute.cpp:暴力程序,不写 freopen,从标准输入读入,结果重定向到 ans1.out;

  3.   poker.cpp:自己的正解,保留 freopen,从 poker.in 读入,输出到 poker.out;

  4.   比较 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

  重要前提:这套脚本必须在考前、在与考场相同的环境下练过至少一遍。 考场上第一次写,很容易在执行权限、./ 前缀、路径或编码等问题上卡住,还不如手造几组数据可靠。

  ⑤ 扫 —— 静态检查代码

  交卷前通读一遍代码,重点检查:

  1.   编译有没有警告(编译时加 -Wall);

  2.   数组大小是否足够,有没有越界;

  3.   int 与 long long 有没有混用出错;

  4.   循环边界是 <= 还是 <,有没有多一或少一;

  5.   == 有没有误写成 =;

  6.   变量名有没有与库函数冲突(见下);

  7.   全局变量与局部变量有没有重名,导致全局变量被屏蔽。

  变量名避坑清单:下列名字作为全局变量名时,在 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 之类的名字。

  ⑥ 净 —— 清理干净再交

  1.   检查所有 cout、printf、puts 语句,删除或注释调试输出(调试请使用 cerr);

  2.   删除试验用的多余文件,提交目录中只保留需要评测的源文件;

  3.   确认 freopen 存在且没有被注释;

  4.   最后再用样例运行一次,确认输出与样例完全一致。

  检查完毕、清理干净,才可交卷。

  板块五 · 稳心篇:跳满错稳,收存平安

  口诀:跳满错稳,收存平安

  串讲:卡住就跳不恋战,每题都写满有输出,编译报错先看第一条,心态稳住不慌乱,最后收手不写新代码,存盘提交要确认,平安交卷出考场。

  ① 跳 —— 卡住就换题,这是策略而不是逃避

  具体做法见板块三④。从心态上再强调一次:在一道题上卡 90 分钟,是复赛中最常见、代价也最大的失误之一。 换一道题做,回头再看时往往思路就清楚了。

  ② 满 —— 每道题都要有代码、有输出

  任何一道题都不应留空。 即使只会输出 -1,也要写好并保存:

  1.   空白必然是 0 分;

  2.   有输出就有得分的可能;

  3.   有些测试点本身比较容易(如“无解时输出 -1”的情形)。

  ③ 错 —— 编译错误(CE)的处理

  1.   先看第一条错误。C++ 的一处错误常常引出一连串报错,改完第一条再重新编译;

  2.   常见原因:缺分号或括号;使用 freopen 却没有包含 (使用万能头文件 可以避免);变量名与库函数冲突;使用了 C++14 中已移除的 gets(可改用 scanf("%s", s) 或 cin);混入中文标点;函数未声明就调用;类型不匹配;

  3.   不使用不熟悉的语法。考场代码以稳为先,在 -O2 -std=c++14 的编译环境下,写法越朴素越好。

  ④ 稳 —— 慌乱时先做有把握的题

  1.   卡住、思路中断时:停下来 30 秒,喝口水,去做一道确定能拿分的题,找回节奏;

  2.   不要和别人比进度。考场上看不到任何人的分数,别人看似在写正解,也可能在调文件读写;

  3.   遇到电脑死机、环境异常:立即举手报告监考老师,不要自行重启或处理。

  ⑤ 收 —— 最后 10 分钟不写新代码

  这段时间写新代码,风险远大于收益:来不及验证,可能引入新的错误,甚至来不及保存。最后 10 分钟只核对收官清单、确认提交。(最后 30 分钟起就不再写新的做法,见板块三⑤。)

  ⑥ 存 —— 随时保存,确认提交位置

  1.   写代码过程中随时按 Ctrl+S 保存,养成写几行就保存一次的习惯;

  2.   每做完一题,确认一次文件的位置和名字;

  3.   使用网页回收系统的省份,结束前 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 分钟内完成,不占用思考时间,把精力全部留给题目本身。

  ③ 模 —— 常用模板清单(考前逐条确认能独立写出)

  1.   基础:排序 sort、二分(lower_bound 或手写)、前缀和、差分、离散化、双指针、单调栈、单调队列;

  2.   搜索:DFS、BFS、记忆化搜索、剪枝、迭代加深;

  3.   图论:邻接表建图、并查集(路径压缩)、Dijkstra(堆优化)、Kruskal 最小生成树、拓扑排序、树上 DFS、LCA(S 组);

  4.   动态规划:线性 DP、背包(01 背包、完全背包、多重背包)、区间 DP、树形 DP、状压 DP(S 组)、最长上升子序列;

  5.   数据结构:树状数组、线段树(S 组)、ST 表、堆与优先队列;

  6.   数学:最大公约数与最小公倍数、素数筛、快速幂、组合数、高精度(J 组偶尔用到);

  7.   字符串: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. 定位错误的步骤

  1.   先找到出错的小数据:用手造的小数据( 取 3~6)找到程序答案与手算答案不一致的一组,后面都用这组数据调试;

  2.   从前往后查,找第一处不一致:先确认读入是否正确,再在程序中间输出关键结果,判断错误出在前半段还是后半段,逐步缩小范围;

  3.   对照手算过程:例如动态规划,先在草稿纸上算出 dp 表的前几项,再与程序输出逐项比较,第一处不同的位置就是转移出错的地方;

  4.   程序运行出错(RE)时:在程序几个位置分别输出“到达位置 1”“到达位置 2”,看最后一条输出出现在哪里,崩溃点就在它之后,再重点检查那里的数组下标。cerr 不经过缓冲,程序崩溃前的输出也能看到,这一点上比 cout 可靠;

  5.   超时(TLE)时:输出循环执行的次数或递归调用的次数,确认是否与复杂度估算相符,判断是哪一段代码执行次数过多。

  3. 哪些数据最值得输出

  1.   读入后的数据:确认读入正确,尤其是多组数据、字符串和边的读入;

  2.   递推或动态规划:每一轮的状态,如 dp[i];二维的 dp 表可以按行输出;

  3.   搜索与递归:进入函数时的参数(当前位置、深度、当前的和等),以及什么时候返回;

  4.   图论:建好的边(逐点输出邻接表),以及 BFS、最短路结束后的 dis 数组;

  5.   模拟:每一步操作之后的关键状态;

  6.   二分:每一轮的 l、r、mid 以及判断函数的结果;

  7.   排序或贪心:排序之后的数组,以及每一步选择了哪一个;

  8.   边界附近:循环开始和结束的几轮,如 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 语句,交卷前再检查一遍(见板块四⑥)。

  ⑥ 不慌 —— 考前熟悉环境,考场才不慌

  1.   提前了解本省的考试系统、编程环境和操作方式(不少省份使用 NOI Linux 2.0,也有省份使用 Windows),参加过本省模拟测试或考前试机的,按当时的操作练熟即可;

  2.   重点熟悉几件事:用什么编辑器写代码、怎样新建文件夹和源文件、怎样编译运行、代码保存在哪里、怎样提交;

  3.   考前一天不熬夜,检查好证件,记牢自己的准考证号。J 组和 S 组的准考证号不同,两场都参加的考生要分别核对。

  附录:考前 24 小时与考场当天

  考前一天

  1.   默写一遍文件框架(freopen 与 return 0);

  2.   默写常用模板清单,写不出的再练一遍;

  3.   做 1~2 道简单的模拟题保持手感,不做大量新题,不熬夜;

  4.   准备好准考证和证件,记牢自己的准考证号;

  5.   把本文的六句口诀抄在纸上,入场前再看一遍(考场一般不允许携带纸质资料,入场前要收好)。

  考场时间安排(以提高级 4 小时为例)

  时间

  做什么

  第 0~5 分钟

  建好三层文件夹,创建四个 .cpp,写好框架和 freopen

  第 5~25 分钟

  通读四题,标难度,定做题顺序

  第 25~75 分钟

  第一梯队:拿下最有把握的题,争取满分

  第 75~145 分钟

  第二梯队:中等题,暴力、特殊性质加部分优化

  第 145~210 分钟

  第三梯队:难题部分分或尝试正解,中途手造几组小数据验证一次

  第 210~230 分钟

  停止写新的做法,按板块四检查各题

  第 230~240 分钟

  按收官清单逐条核对,确认四题都已保存或提交

  手造小数据验证是基本动作。采用“先稳拿前两题、后两题拿部分分”策略的考生,可以在后两题的部分分写完后,把剩余时间用于给前两题对拍。

  考前一分钟,默念六句口诀

  一保二骗三取舍,四验五稳六装齐

  保分:文界初算,归格不卡

  骗分:暴特表猜,拼分卡时

  取舍:读排保跳,查稳收官

  验证:验边极拍,扫净方交

  稳心:跳满错稳,收存平安

  装备:快文模卡,调试不慌

  最后: 复赛拿高分的考生,往往不是算法最强的,而是不爆零、拿足部分分、时间安排合理的。把这六句口诀记熟,就能在细节上少丢分。

  祝各位考生:文件全对,样例全过,暴力都得分,正解都想通。

  免责声明

  本文由新东方优编程教练团结合 CSP-J/S 第二轮公开考试规则、历年公开真题及日常教学与集训经验整理,内容为考前复习方法与考场策略建议,仅供备考参考,不构成对考试成绩的承诺。考试时长、编译选项、考生目录结构、文件命名、提交方式及评测规则等具体要求,各省可能存在差异,且可能逐年调整,均以 CCF 官方及本省认证点发布的考试通知、考场说明和监考老师的要求为准。文中的时间分配、复杂度估算与代码示例为通用参考,考生应结合自身水平和具体题目灵活调整。如遇争议,以 CCF 官方解释为准。

  新东方优编程教练团 · 2026 年度教研出品

免责声明免责声明:以上内容来源于互联网,如有侵权请联系我们删除。