1. 2023年CSP-J初赛这份卷子:结构稳定,但风向在变
1.1 先看卷面:120分钟100分,三块题型怎么分
2023年CSP-J第一轮认证的整体形式没有大幅调整,卷面满分100分,考试时间120分钟。第一部分是15道单选题,每题2分,合计30分;第二部分是3道阅读程序大题,每道题后面跟几道判断题和选择题,合计40分;第三部分是2道完善程序大题,每道题留出若干个空,要求从四个候选代码片段里选出最合适的填入,合计30分。从考点分布来看,计算机基础知识占5到6题,进制转换与逻辑运算占3到4题,语法与数据结构占5到6题,算法初步内容占2到3题。
这里我想多说一句容易被忽视的策略问题:阅读程序和整段程序填空加起来足有70分,但不少选手备考时把大部分时间花在背诵单选的零散知识点上,这个投入产出比其实是倒挂的。单选30分里确实有一部分是纯记忆题,可记忆题的考点非常固定,考前两三周集中背完全来得及;真正决定你能否过线的,是阅读程序题能不能稳定拿分,以及完善程序题能不能根据代码上下文推出算法思路。
1.2 和往年卷子相比,三个信号很显眼
第一个信号是计算机基础题变得更"场景化"。前些年的卷子喜欢直接问"1KB等于多少字节""CPU由哪几部分组成",答案一眼就能扫出来。2023年的题目把类似知识放进了具体应用场景里,题干明显变长,不少选项带着生活中的描述。这对真正用过电脑、装过软件、跑过程序的选手更有利,也意味着只靠背诵手册打天下的备考方式需要调整。
第二个信号是STL内容在阅读程序题里的出场率变高。sort、vector、结构体排序、pair这些标准模板库内容,往年更多出现在完善程序题的备选答案中,2023年则直接出现在阅读程序题的代码里。如果平时刷题只写纯C风格的数组和循环,对STL没有形成条件反射,读这类代码会明显慢半拍,甚至被一堆模板参数绕晕。
第三个信号是完善程序题越来越像"竞赛模板题的填空版"。二分查找、贪心区间、最长连续上升子段、差分数列这些信息学入门阶段的经典内容,成为填空的主要素材。换句话说,只学语法不刷算法题的人,完善程序题基本只能靠蒙;反过来,只要系统刷过一两百道入门算法题,这些填空的思维链路几乎是透明的。
1.3 难度定位:比2022年友好,但区分度更高
整体来看,我认为这份卷子的难度属于近三年中等偏低,但区分度反而是近三年最高的。原因在于题目分层非常清晰:至少40分是"认真学过编程就能拿稳"的基础分,40分是"需要现场推演但不算刁钻"的中档分,最后20分才是真正拉开差距的内容,集中在阅读程序题最后一道和完整程序题第二道。这三层分清楚之后,学过和没学过的人会在50分左右形成明显的分水岭,学得扎实和学得粗糙的人又会在85分以上继续分层。
所以我的判断是:如果目标只是过线进第二轮,这个难度对认真准备过的学生很友好;如果目标是拿高分,考前突击几天是不够的,必须靠平时刷题的积累。这也是每年初赛最公平的地方——它考的从来不只是一个"会不会背",而是"有没有真正动手写过代码"。
2. 单选题精讲:把30分稳稳吃进嘴里
2.1 计算机基础题:背过就能拿分,但要看清题干
每年单选都有四五道计算机基础题,2023年也没有例外。输入输出设备、存储器层次、操作系统分类、常见文件格式这些考点都在正常范围内。题目本身不难,但很多选手在本该拿分的地方翻车,原因不是不会,而是没看清题干。
举个例子,一道很典型的题:
下列哪个选项中的两个都属于输出设备? A. 键盘、鼠标 B. 显示器、打印机 C. 扫描仪、显示器 D. 麦克风、音箱
答案是B。键盘、鼠标、扫描仪、麦克风属于输入设备,把外界信息送进计算机;显示器、打印机、音箱属于输出设备,把计算机处理后的结果呈现出来。这种题只要概念清楚,十秒内就能做完。真正容易错的是它换个问法,比如"下列哪个既是输入设备又是输出设备",答案应该是触摸屏或网卡。备考时不要只背单个设备的分类,要把"信息流向"这一本质记住,遇到任何新设备都能推断。
存储器相关题目也是高频考点。寄存器、缓存、内存、外存的速度和容量关系,可以记成一条链:寄存器最快、容量最小,越往下速度越慢、容量越大。考试常考"断电后哪部分数据会丢失",只要知道寄存器和内存里的数据是临时存储,硬盘上的数据是持久存储,这个点就不会错。操作系统类的题目更简单,关键是把Python、Scratch这类编程语言和操作系统区分开,看到这种选项基本就是送分。
2.2 进制、编码与逻辑运算:算得慢才是最大问题
进制转换是选择题里少有的"需要动笔计算"的题,细分下来有两种考法:二进制转十进制、十进制转二进制,偶尔带一点十六进制。这类题不存在会不会的问题,只看算得准不准、稳不稳。
我建议每个人只练熟一种方法,不要混合使用。十进制转二进制就用短除法,一直除到商为0,把余数从下往上排;二进制转十进制就用位权展开法,从最低位开始每位乘以2的幂再求和。比如二进制101101转十进制,从右往左是1×2^0 + 0×2^1 + 1×2^2 + 1×2^3 + 0×2^4 + 1×2^5 = 1 + 4 + 8 + 32 = 45。整个过程在草稿纸上写清楚,不要心算,心算在紧张状态下特别容易漏幂次。
十六进制和二进制之间的转换也要熟练,规则是每一位十六进制数对应四位二进制数,比如十六进制2F转二进制就是0010 1111。2023年的卷子里进制题不是最难的,但它和编码题经常搭配出现,比如问ASCII码、汉字编码或Unicode的基本概念。这类题的坑点在于选项中喜欢混杂"编码方式"和"字符集"的概念,备考时把ASCII、GBK、UTF-8各自是什么场景下用的搞清楚就够了。
逻辑运算部分重点看短路求值。C++里a && b这种表达式,如果a为假,b根本不会执行;a || b中如果a为真,b也不会执行。把这个特性理解清楚,阅读程序题里遇到带有赋值语句的逻辑表达式就能快速判断。
2.3 语法与数据结构:栈、队列、树是永远的C位
数据结构在单选里基本围绕栈、队列、二叉树展开。栈的经典考法就是给入栈序列,问哪个出栈序列不可能出现。我在教学中会让学生先记一条原则:某个元素出栈时,它上面所有元素必然都已经出栈。比如入栈序列是1、2、3、4,如果第一个出栈的是4,那么剩下只能是3、2、1依次出栈,绝对不可能出现4、1、2、3这种顺序,因为1压在2和3下面,被4挡住出不去。
二叉树的最大考点是遍历序列。给前序加中序求后序,或者给中序加后序求前序,是每年单选压轴区域的常客。这类题的完整解法是画图:前序序列的第一个节点一定是根,中序序列中根节点把左右子树分开,然后递归处理左子树和右子树。举个例子,前序遍历为ABDEC,中序遍历为DBEAC,前序第一个A是根,中序里A左边的DBE是左子树,C是右子树;左子树前序是BDE,中序是DBE,所以B是左子树的根,D是左孩子,E是右孩子。后序遍历是左右根,最后得到DEBCA。整个推导过程不超过一分钟,比靠感觉猜要可靠得多。
队列的考法相对简单,重点是"先进先出",常和广度优先搜索绑定出现。链表在2023年没有单独出大题,但数组模拟链表的思路在阅读程序题里偶尔出现,理解next数组的含义比背链表定义更重要。
2.4 组合数学与逻辑推理:拉开差距的分水岭
单选最后几道题通常放组合数学或逻辑推理,这也是30分里最不好拿的部分。组合数学其实就是排列组合的简单应用,比如"5个人排成一排有几种排法""从5个人中选2个人有几种选法"。前者是排列,答案是5!;后者是组合,答案是C(5,2)=10。带限制条件时会稍微复杂,比如"甲乙必须相邻",解法是把甲乙捆成一个整体再和其他对象排列;"甲乙不相邻",则用总数减去相邻的情况。这些模型在信息学竞赛里会反复出现,值得在备考阶段系统地整理一次。
逻辑推理题更像小奥里的真假话问题,通常给出几条条件,要求判断人物或物品的对应关系。我的建议是绝对不要在大脑里空想,一定要在草稿纸上画表格,一行一列明确标出条件。每条条件就相当于给表格中的某些格子打钩或打叉,推理到最后自然会剩出答案。这种题虽然放在最后,但分值也只有2分,如果在上面卡了超过五分钟,建议先跳过,把时间留给后面的程序题,毕竟那边是70分的大头。
3. 阅读程序题:不运行代码,也能完整推导出答案
3.1 第一类程序:循环取数与计数,先抽象功能再代入数据
阅读程序题的第一大题通常是一段中等长度的代码,难度不大,但选项里往往混着"功能是什么"这类概括题,以及"当输入为某值时输出是什么"这类模拟题。我做题时有一个固定的顺序:先通读整段代码,问自己一句"这段程序到底在算什么",然后才动手代数据。如果一上来就逐行模拟,很容易陷进细节,算到最后甚至忘了程序的目标。
用一道接近真题风格的题目来演示:
#include <iostream> using namespace std; int main() { int n, cnt = 0; cin >> n; for (int i = 1; i <= n; i++) { int t = i; while (t > 0) { if (t % 10 == 7) { cnt++; break; } t /= 10; } } cout << cnt << endl; return 0; }先抽象功能:外层循环从1到n枚举每个整数,内层while不断把t除以10,实际上是在拆出每一位数字;遇到底7的条件时cnt加1,然后break跳出循环。所以这段程序统计的是"1到n之间十进制表示中含有数字7的数的个数"。关键在于break保证了每个数最多只计数一次,像77这种含两个7的数不会被重复计算。
当n等于20时,1到20中含7的数只有7和17两个,输出就是2。如果改成n等于100,可以用分段法快速计算:十位含7的数有70到79一共10个,个位含7的数有7、17、27、37、47、57、67、87、97一共9个(77已经在十位含7里算过),加起来是19个。这种题用逻辑推导远比一行行模拟整个循环要快,而且不容易错。阅读程序题的通用套路就是:先把代码"翻译成人话",再用数据验证。
3.2 第二类程序:递归函数,画调用树比跟踪变量更有效
递归题是很多选手的噩梦,因为它需要同时跟踪多层调用,大脑很容易过载。但递归题也是最有规律可循的,几乎都可以用"函数调用树"来解决。以斐波那契型递归为例:
#include <iostream> using namespace std; int f(int n) { if (n <= 1) return n; return f(n - 1) + f(n - 2); } int main() { int n; cin >> n; cout << f(n) << endl; return 0; }这段代码计算的是第n个斐波那契数:f(0)=0,f(1)=1,f(2)=1,f(3)=2,往后依次是3、5、8。问f(6)的输出,只要在草稿纸上画一棵树:f(6)分成f(5)和f(4),f(5)又分成f(4)和f(3),一直展开到f(1)和f(0)这两个边界,然后把所有叶子节点的值加起来。画树的另一个好处是能回答"某个子函数被调用了几次"这种附加问题。比如f(6)的计算过程中f(3)被调用了多次,数树上f(3)的节点数量即可。
递归题里还有一个常见考点是边界条件。如果代码里写成if (n <= 2) return 1,整个数列就变成了1、1、2、3、5,和标准斐波那契差了一个位置。出题人很喜欢在这种地方埋坑,所以遇到递归题一定要看清楚边界条件返回的是几。2023年的阅读程序题中,递归和循环交替出现在三道大题里,凡是碰见函数调用自己,画调用树是性价比最高的解法。
3.3 第三类程序:结构体排序与STL组合,读代码要抓住比较规则
今年阅读程序题里最值得注意的趋势是结构体排序代码直接出现。这类代码看起来长,真正核心的只有一个点:排序的比较规则是什么。看下面这个片段:
#include <iostream> #include <algorithm> using namespace std; struct node { int x, y; } a[1005]; bool cmp(node p, node q) { if (p.x != q.x) return p.x < q.x; return p.y > q.y; } int main() { int n; cin >> n; for (int i = 0; i < n; i++) cin >> a[i].x >> a[i].y; sort(a, a + n, cmp); for (int i = 0; i < n; i++) cout << a[i].x << " " << a[i].y << endl; return 0; }cmp函数定义的是"什么样的顺序算合法":先按x从小到大排,如果x相同,再按y从大到小排。输入三组数据,分别是(2,5)、(1,3)、(2,1),排序后的输出就是(1,3)、(2,5)、(2,1)。这道题本身不难,但很多选手对sort和cmp的理解只停留在"能跑就行",一看到函数内部的引用符号和比较逻辑就懵。这里要注意:cmp返回true时表示第一个参数应该排在第二个参数前面,千万不要把排序规则记反了。
这类题还有两个容易被忽略的小地方:一是排序是否包含第0个元素,这里sort(a, a+n, cmp)包含a[0]到a[n-1],共计n个元素;二是数组下标从0开始,遍历输出时也是从0到n-1。2023年的阅读程序题把这两个小坑和结构体排序叠加在一起,不多花十秒核对的人很容易选错。
3.4 阅读程序题的实操建议:三个动作不能省
第一,通读代码时拿一支笔,把每个循环的起点、终点、步长标出来,特别是嵌套循环的外层和内层关系。第二,判断题里出现"可能""一定""恰有"这些程度词时,不要凭感觉选,用一个小数据亲自跑一遍边界,比如n=0、n=1、数组只有一个元素这些极端情况。第三,选择题如果让你选"程序功能",先看整体再看局部,不要被中间某个变量的名字误导。char变量不一定存字符,flag不一定表示标记,变量名只是给人看的,代码行为才决定一切。
我在带学生的过程中发现,阅读程序题从"能看懂"到"能拿分"之间,缺的往往不是知识,而是耐心。很多人读代码三分钟就烦了,直接跳到问题去猜答案。实际上每道阅读程序题都有至少两三个小问是完全按代码运行结果出的,只要跟着代码走一遍就能拿到。初赛的阅读程序题对大题思维的要求并不高,关键是稳。
4. 完善程序题:每一个空背后都是算法骨架
4.1 先判断算法类型,再看填空,别顺序颠倒
完善程序题是初赛里最考验算法功底的板块,因为它不是考"填一个语法正确的语句",而是考"填出能让程序完成目标的那句话"。很多选手一上来就盯着空格选项看,这是最大的误区。正确做法是先读题目描述,搞清楚程序要解决的问题,然后快速判断它属于哪类算法:有序数组里找位置是二分,区间选最多不重叠是贪心,求方案数或最优值且存在重叠子问题是动态规划。
判断算法类型之后,你就有了一个"预期的模板"。比如提到二分查找,你心里应该自动浮现出l、r、mid、循环条件、收缩方向的完整骨架。之后再去看空格,每个空其实对应模板里固定的某个位置,选项里那些干扰项虽然语法都对,但逻辑上不匹配模板,一眼就能排除掉。这比在没有上下文的情况下逐个选项试错要快得多,也准得多。
4.2 用"逻辑锚点法"定位每个空,四个角度的检查
我把完善程序题的解题过程总结成四个锚点,在考场上按顺序过一遍,正确率会明显提升。
第一个锚点是初始化。看程序开头的变量定义和首个赋值,判断它和问题描述中的初始状态是否一致。比如求最大值,ans通常初始化为一个很小的数或第一个元素;求连续子段长度,cnt通常初始化为1。如果空填在一个变量第一次使用之前,那你选的就是初始化语句。
第二个锚点是循环和递归的边界条件。循环继续的条件是什么,什么时候停止,在哪个条件下越界。边界条件填错是失分最严重的地方,因为程序往往仍能运行,只是答案不对。解决方法是代入特殊数据,比如n=1、l=0、数组长度为1,看看循环是否还能正确退出。
第三个锚点是分支条件。程序里出现if或else时,要判断它对应的是问题的哪一次决策。是满足某种性质时进入if,还是不满足时进入if。把问题描述里的关键条件和代码里的判断对应起来,分支类的空基本不会选错。
第四个锚点是更新语句。在循环内部或分支内部,变量如何变化才能逐步逼近最终答案。这里的检查方法是让程序"跑"一次小数据,比如n=3,所有变量都记下来,看更新逻辑是否符合预期。
4.3 二分查找模板:最常出现的填空素材
二分查找在完善程序题里的出场率极高,因为它既是算法基础,又可以变化出多种考法。看一个在升序数组中找第一个大于等于x的位置的模板:
#include <iostream> using namespace std; int main() { int n, x; int a[100005]; cin >> n >> x; for (int i = 1; i <= n; i++) cin >> a[i]; int l = 1, r = n, ans = n + 1; while (l <= r) { int mid = (l + r) / 2; if (a[mid] >= x) { ans = mid; r = mid - 1; } else { l = mid + 1; } } cout << ans << endl; return 0; }如果把它出成完善程序题,空格通常会设置在l和r的初值、while条件、a[mid]与x的比较方式,以及两个分支里r和l的更新方向。很多选手容易把r = mid - 1和l = mid + 1填反,导致死循环或漏掉答案。我的记忆方式是:当a[mid]已经满足了"大于等于x"时,mid可能是答案,但左侧可能还有更小的满足条件的数,所以把右边界收到mid左边继续找;当a[mid]小于x时,mid以及它左边都不可能满足条件,所以把左边界收到mid右边。理解了这个逻辑,两个更新语句就不会错。
这道题还要注意ans初始化为n+1的意义:如果整个数组都小于x,说明不存在这样的位置,输出n+1表示在数组末尾之后。这个设计是程序完整性的体现,也是填空中常考的初始化语句。
4.4 贪心区间问题:另一个高频模板
2023年的完善程序题里,贪心思想占了不少比重。区间选点、区间覆盖、活动安排这类题目在洛谷上有大量原题,比如P1803"凌乱的yyy"几乎是入门必刷。这类题的核心模板是:把所有区间按右端点升序排序,然后从左往右扫描,记录上一个已选区间的右端点,遇到左端点大于等于它的区间就选。核心代码如下:
#include <iostream> #include <algorithm> using namespace std; struct seg { int l, r; } s[1005]; bool cmp(seg a, seg b) { return a.r < b.r; } int main() { int n; cin >> n; for (int i = 1; i <= n; i++) cin >> s[i].l >> s[i].r; sort(s + 1, s + n + 1, cmp); int ans = 0, last = -1; for (int i = 1; i <= n; i++) { if (s[i].l > last) { ans++; last = s[i].r; } } cout << ans << endl; return 0; }完善程序题的常见挖空位置有三个:cmp函数里的比较方式、last的初始值、以及判断不重叠时的条件。比较方式选按右端点升序,这是贪心正确性的关键;last初始化为-1是在所有区间左端点都可能为0的情况下,保证第一个区间一定被选中;判断条件写成s[i].l > last还是s[i].l >= last,取决于题目说区间端点能否重合。做题时先读题描述中"重叠"的定义,再决定用大于还是大于等于,这样才能避开出题人设置的陷阱。
5. 从2023年试卷反推的复习清单与考场操作
5.1 按优先级排序的知识点清单
每年考完都有人问我"明年初赛该复习什么",我的回答从来都是分层准备。底层是必拿分,不复习就亏;中层是重点拿分,需要刷题巩固;顶层是冲刺分,有余力再去攻克。我把这套清单按2023年的考点分布整理成了一张表,方便直接对照:
| 优先级 | 知识点 | 对应题型 | 建议投入时间 |
|---|---|---|---|
| 一级 | 计算机基础概念、输入输出设备、存储单位 | 单选 | 考前两周集中背 |
| 一级 | 进制转换、十六进制与二进制互转 | 单选 | 每天练10道,一周达标 |
| 一级 | C++基本语法、循环、数组、变量作用域 | 单选、阅读程序 | 日常刷题积累 |
| 一级 | 栈、队列、二叉树遍历 | 单选、阅读程序 | 画图理解原理 |
| 二级 | 结构体与sort自定义排序 | 阅读程序、完善程序 | 刷10道排序题 |
| 二级 | 二分查找模板 | 完善程序 | 手写模板5遍 |
| 二分 | 差分数列、前缀和 | 完善程序 | 学完就练 |
| 三级 | 贪心区间问题 | 完善程序 | 刷P1803等经典题 |
| 三级 | 动态规划入门(线性DP) | 完善程序 | 能写出状态转移即可 |
之所以把二分和差分数列放在二级,是因为它们是完善程序题里出现频率最高的算法素材。把模板背下来还不够,要理解每一步为什么这样写,因为出题人会改变数组起点、改变比较条件、改变返回值,模板不变但细节可以千变万化。
5.2 考场上的时间分配与做题顺序
120分钟听起来很多,但实际做完三块题型会发现时间很紧张。我的建议是拿到试卷后先花两分钟把所有题目快速扫一遍,标记出"立刻能做"和"完全没思路"的题,然后先做有思路的。单选题整体控制在25分钟以内,做完立刻涂卡,不要回头反复改,第一直觉往往是准确的。
阅读程序题建议给到50到55分钟,这是拿分的主力区。做题时先看程序功能性问题,再看具体的输入输出判断题。我见过很多选手在判断题上反复纠结,花了一个小时,结果后面完善程序题没时间做。判断题的分值通常是每题2到3分,和单选题差不多,不值得用十分钟去赌一个不确定的选项。完善程序题留40分钟,最后10分钟统一检查答题卡填涂,避免出现串行涂错的低级失误。
5.3 高频失分原因与防坑策略
第一,读程序时只盯细节不抓功能。很多选手能从代码里算出某个变量的值,但问"程序的功能"时反而答错,因为整个推演过程没有形成整体认识。防坑方法是在读代码前先默读一遍题目要求,带着"它要解决什么问题"的预期去读,而不是漫无目的地逐行模拟。
第二,忽略"可能"和"一定"的区别。阅读程序题里常出现"该程序的功能可能是""下列说法一定正确的是"这类措辞,选项里可能同时出现两个看起来都对的说法,但一个用词太绝对,逻辑上有反例。遇到这种题,用极端数据去验证每个说法,比如n=0或数组全相等时的行为,往往能一锤定音。
第三,完善程序题不看初始化就直接填循环里的空。比如贪心区间题,last初始化为-1还是初始化为第一个区间的右端点,会直接影响循环内的判断逻辑。如果只盯着空格前后的几行看,很容易被选项带偏。先把整段代码在脑海里读到完整,再动手选。
第四,时间分配失衡。单选里遇到一道逻辑推理题卡了五分钟还不放手,后面阅读程序题就只能连蒙带猜。我的底线是:任何单选题思考超过三分钟立刻跳过,整套卷子做完后再回来处理。留得青山在,不愁没柴烧,这句话放在初赛考场上非常实用。
从2023年这份卷子往回看,CSP-J第一轮认证真正想筛选的,从来不是背书机器,而是那些能理解代码、能补全算法思路、能在一百二十分钟内保持冷静的人。我个人的体会是,与其考前焦虑地去搜各种所谓的押题卷,不如把最近三到五年的真题踏踏实实做两遍,再把每一道做错的题背后的知识点回补到自己的笔记里。2023年给了我们一个很清晰的提示:刷题量决定你的下限,而复盘深度决定你的上限。