CSP 第一题在选手圈子里有个外号叫"签到题",意思是你进考场先把这题的分揣兜里,再去啃后面那些真正要动脑子的。但有意思的是,每年考完总有人在群里喊"第一题只过了 60%",问题往往不是不会写,而是踩了某个不起眼的坑。这篇汇总就是把我这些年刷 ccf_csp、整理 CSP 第一题的经历摊开讲,把历年第一题的题型、套路、读入输出细节、边界条件一次性梳理清楚。不管你是第一次报名、还在纠结用 C++ 还是 Python 的同学,还是刷了几套真题但总觉得分数不稳的老手,都能从里面找到自己缺的那块。下面不聊虚的,直接从评分机制、题型分类、代码骨架一路讲到上场时的时间分配。
1. CSP 第一题的定位:签到题为什么成了很多人的唯一"保底分"
我观察过一个现象:不少同学报名之后的前两周,热情全砸在第四题、第五题上,结果练到考前发现自己连第一题的边界条件都还没吃透。等到真正上机,时间一紧,第一题卡了个细节,心态直接崩,后面几题也跟着黄了。所以我想先把第一题的定位讲清楚,这比急着敲代码重要得多。
1.1 评分机制决定了第一题的战略价值
CSP 认证满分 500 分,一共五道题,每道题 100 分。题目按难度单调递增,第一题几乎是所有场次里最友好的一道。它的典型特征是:题意直白、数据范围小、算法简单,很多情况下一个双重循环就能搞定,甚至不需要任何数据结构。
这就带来一个很实际的问题——第一题是"必须拿满"的部分。为什么这么说?后面四题里,第四题、第五题往往涉及图论、动态规划、复杂数据结构,写出来未必对,对了未必过全部测试点。而第一题不一样,它的测试点设计基本不会为难你,只要你把题意理解对、边界条件处理好,拿满 100 分是常规操作。
我见过的分档大概是这样的:能稳定拿第一题满分的人,认证分数至少有个 100 分兜底;再往上每多拿一道题,档次就明显不同。所以对大多数以"过线"或"稳一个体面分数"为目标的人来说,第一题的性价比是最高的。花两个小时死磕第三题,不如花二十分钟把第一题的历年题型过一遍,把该拿的分稳稳锁死。
1.2 历年第一题的共同特征
我把这些年第一题的共性总结成四点,理解了这四点,你面对任何一道没做过的新题都能快速判断该往哪个方向想。
第一,输入规模小且规整。第一题的数据量通常在几百到几千这个量级,很少出现需要优化到 O(n log n) 的情况。这意味着暴力解法基本都安全,你不需要为了性能去设计精巧的算法。
第二,题意没有歧义陷阱。题目描述会非常明确地告诉你"求什么、按什么规则、输出什么格式"。它不会像第四题那样藏着一层抽象建模。你读题读两遍基本就能理解它要干嘛。
第三,考察的是工程实现而非算法设计。第一题真正考的是你会不会老老实实地把流程翻译成代码,能不能处理好多行输入、能不能注意整数溢出、能不能把输出格式对齐。这些是"实现能力",不是"算法能力"。
第四,大多数题可以一次遍历解决。无论是统计频次、模拟流程还是做简单计算,核心逻辑往往就是一个循环,配合几个变量。想清楚用什么变量、在哪里更新,题就做完了一大半。
把定位摆正了,接下来才好谈具体题型。很多人一上来就背代码,结果换个题面就懵,就是因为没建立起"看到题面就能归类"的直觉。
2. 把历年第一题拆开看:五类题型与它们的识别信号
刷了这么多套真题之后,我发现第一题其实就那么几副面孔,来来回回。下面这五类基本覆盖了绝大部分场次,每一类我都给出识别信号和对应的真题例子,你以后看到类似题面就能立刻知道该套哪套思路。
2.1 计数与频次统计类
这类题的标志性问法是"出现次数最多的数""统计每种情况出现了几次""求出频率"。代表题目包括"出现次数最多的数""门禁系统""灰度直方图""词频统计"。
核心套路非常简单:用一个数组或哈希表做计数器。如果值域是有限的、且范围不大,直接开一个数组,下标就是被统计的值;如果值域很大或者是字符串,就用哈希表映射。
举个经典例子,"出现次数最多的数"要求找出出现次数最多的数,如果并列则取最小的那个。标准做法是排序之后一次遍历统计连续相同元素的个数,同时维护最大次数和对应最小值;或者用一个计数数组,边统计边比较。这类题最容易出的岔子是"并列时取最小"这个附加条件,很多人只统计了最大次数,忘了二级排序规则,结果测试点里专门有一组并列数据。
2.2 流程模拟与状态推进类
这类题会描述一个有时间顺序的过程,让你按步骤模拟。"跳一跳""小明上学""报数""分蛋糕"都属于这一类。它们的信号是题面里出现"依次""每一轮""当满足某条件时"这样的推进式描述。
解这类题的关键在于找到一个干净的状态表示。比如"跳一跳"里,你需要记录当前是第几次落在方块上、连续落在中心多少次,然后根据落点位置决定得分。"报数"里要维护当前报到几、几个人报过、几个人跳过。状态变量选对了,模拟循环就是照抄题意;状态选乱了,代码就会绕成一团。
我个人的经验是,遇到模拟题先拿纸画两三步,把每一步状态怎么变写下来,再动手。别一上来就敲,模拟题改起来最烦,前面状态设计错了后面全得推倒。
2.3 公式推导与增量计算类
这类题表面像模拟,实际上藏着一个可以化简的公式。"打酱油""数列分段""现值计算""如此编码""序列查询"都有这个味道。
拿"打酱油"举例,规则是买的数量和赠送的数量之间有档位关系,你可以直接按档位贪心,也可以用一点数学分析每档的性价比。而"现值计算"是让你按年折现求总和,本质就是一个幂次累加,看清公式就能一次遍历完成。
这类题的识别信号是:题面用自然语言描述了一堆规则,但这些规则其实可以用一个表达式概括。你要做的是把它翻译成数学语言,而不是逐条 if-else 硬套。虽然硬套也能过(毕竟第一题数据小),但推导一遍能让你写得更短、更不容易错。
2.4 坐标变换与几何计算类
"图像旋转""坐标变换""田地丈量""称检测点查询""线性分类器"属于这一族。它们的共同点是涉及二维平面上的点、线、矩形或矩阵。
坐标系类题目有两个反复出现的陷阱。一个是旋转和翻转的方向,题目里说"顺时针旋转 90 度",你得确认输出的行和列哪个对应哪个,稍不注意就转反了。另一个是边界是否包含,比如矩形交集面积计算,"田地丈量"里给定的矩形是否包含边界点,直接影响你代入的是开区间还是闭区间逻辑。
以"图像旋转"为例,顺时针旋转 90 度后,原来的第 i 行第 j 列元素会落到新的位置,你需要先推导出映射关系再写循环,而不是凭感觉调下标。这类题我建议先在草稿纸上画一个 3×3 的小矩阵,手动转一遍,把映射写对再写代码。
2.5 字符串匹配与状态枚举类
"重复局面""密码""未初始化警告"是这一类的代表。它们涉及对字符串或状态集合的比对与记录。
"重复局面"是让你判断某个棋盘局面的字符串之前是否出现过,标准做法是把每个局面拼成一个字符串,扔进一个集合里查重。"未初始化警告"是记录哪些变量被赋值过,同样用集合。这类题的关键是设计一个能唯一表示状态的字符串或编码,只要编码不冲突,后面的查重就是一行集合操作。
把题型分完,你会发现第一题的解法库其实很窄。接下来要讲的读入输出,才是真正让分数忽高忽低的地方。
3. 读入与输出:第一题真正阴人的地方
算法都是小学生的难度,但每年都有人在读入输出上翻车。我见过太多"算法全对、格式全错"的悲剧,所以这一节我讲得细一点。
3.1 数据读入方式的选择
CSP 的官方输入格式很规整:第一行往往是 n 或者 n m,接下来是具体数据。用 C++ 的话,cin和scanf都能用,性能上第一题的数据量根本不需要纠结。我个人的习惯是用cin配合ios::sync_with_stdio(false)提速,写起来更清爽。
真正的坑在于输入的行结构和你的读法不匹配。比如某题说"第一行一个整数 n,接下来 n 行每行两个整数",而有人写成了先读 n 再一次性读 2n 个数,逻辑上等价,但如果中间夹着别的信息就会错位。还有一种情况是输入里有多余的空格或换行,用cin >>会自动跳过空白,不会有问题;但如果你用getline读字符串,前面残留的换行符会吃掉你的第一行,这是新手最常犯的错。
用 Python 的话,读入要更小心。input()一次读一行,遇到大量数据用sys.stdin.read().split()一次性切分更稳。如果题目是逐行给数据,而你用了一次性读入再手动分块,一定要确认分块逻辑和行结构一致。
3.2 输出格式的隐藏要求
输出格式的坑比想象中多。常见的几种:
- 末尾不能有多余空格。有些评测系统对行尾空格不敏感,但有些会判错,稳妥做法是最后一个元素单独输出或在循环里判断。
- 大小写和标点。题目说输出"YES"你就不能输出"Yes",说输出整数你就不能带小数点。
- 多组输出之间的换行。有的题要求每组答案占一行,有的要求空格分隔,读题时一定要圈出来。
我整理了一个格式检查清单,每次交卷前扫一眼:
| 检查项 | 常见错误 | 正确做法 |
|---|---|---|
| 行尾空格 | 循环里无条件加空格 | 首元素特殊或末尾单独处理 |
| 大小写 | 输出 Yes 而非 YES | 严格照题面 |
| 整数精度 | 用浮点输出整数 | 确认数据类型 |
| 换行数量 | 多输出一个空行 | 按题目要求控制 |
| 并列规则 | 忽略"取最小"等附加条件 | 读题时标红 |
这些看起来是小事,但它们恰恰是第一题拉开差距的地方。算法谁都写得出来,格式处理得干净的人才拿满分。
4. 各类题型的解题骨架与代码模板
光讲套路不给骨架等于白讲,这一节我把前面几类题型的标准写法整理成可复用的骨架。注意,骨架是帮你想清楚结构,不是让你背下来套,具体题目一定要按题意调整。
4.1 统计类的通用骨架
统计类题目基本都是"开一个容器,遍历输入,边遍历边更新计数和答案"。
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; map<int, int> cnt; // 值 -> 出现次数 int bestVal = 0, bestCnt = 0; for (int i = 0; i < n; ++i) { int x; cin >> x; cnt[x]++; // 边统计边更新答案,注意并列取最小的规则 if (cnt[x] > bestCnt || (cnt[x] == bestCnt && x < bestVal)) { bestCnt = cnt[x]; bestVal = x; } } cout << bestVal << "\n"; return 0; }这段骨架的关键点是边读边更新,而不是读完再遍历一遍。虽然两种写法都能过,但边读边更新省一次循环,也逼着你在读的时候就把并列规则想清楚。
如果值域很小(比如灰度值 0 到 255),把map换成定长数组会更快也更简单,这就是"灰度直方图"的标准做法——开一个大小为 256 的数组,读到一个值就加一。
4.2 模拟类的通用骨架
模拟类的结构通常是"维护若干状态变量 + 一个推进循环"。
int main() { int n; cin >> n; int score = 0; // 总分 int combo = 0; // 连续命中次数 for (int i = 0; i < n; ++i) { int x; cin >> x; if (x == 1) { combo = 0; // 落空,连击清零 } else if (x == 2) { combo++; score += 1 + 2 * (combo - 1); // 按题意计算得分 } } cout << score << "\n"; return 0; }模拟题骨架的要点是:把每个状态变量的含义写清楚,最好在变量名上体现(如combo表示连击数)。当你写到一半发现某个变量职责不清时,停下来想清楚再继续,别硬编。
我还想强调一点,模拟题的状态更新顺序很重要。有些题里,同一轮内多个状态的变化有先后依赖,顺序错了结果就错。遇到这种,先把状态变化画成步骤图,再翻译成代码。
4.3 数学推导类的通用骨架
推导类题目没有统一骨架,但有一个通用做法:先把题目里的规则写成数学表达式,再决定怎么遍历。
以"现值计算"为例,题目给一串现金流和折现率,要求现值和。核心公式是每一年的现金流除以 (1+r) 的若干次方。一个朴素做法是每年都重新算一次幂,虽然数据小没问题,但更优雅的做法是维护一个累积的折现因子:
double factor = 1.0; double total = 0.0; for (int i = 0; i < n; ++i) { total += cash[i] * factor; factor /= (1 + rate); }这就是增量计算的思想——把重复的幂运算换成一次除法,既快又不容易累积误差。这类小技巧在第一题里用不用都行,但养成习惯后你在后面几题会更顺手。
5. 边界与数据范围:第一题丢分的集中区
如果说读入输出是第一题的第一大坑,那边界条件就是第二大坑。我统计过自己早期刷题的错误记录,光"数据范围没注意"这一条就占了将近三分之一。
5.1 常见的数据范围陷阱
第一题里最常见的几类范围问题:
整数溢出。CSP 第一题的数值往往不大,但不代表可以无脑用int。比如求几个数相加,如果有几百个数每个都接近 int 上限,累加就会溢出。稳妥做法是,只要涉及求和、乘积,就问问自己"最坏情况会不会超过 2^31-1",超过就用long long。
边界为空的特殊情况。有些题 n 可以取到很小,比如 n=1。这时候"相邻""配对""连续段"这类逻辑都会退化。我印象很深的是"相邻数对"这类题,如果只有一个数,根本不可能有相邻数对,答案应该是 0——但如果你写循环时想当然地从下标 0 遍历到 n-1 再访问a[i+1],就会越界。
并列与最值的判定顺序。前面反复提到的"并列取最小"就是典型。类似的还有"求最大波动"里相等的处理、"求中间数"里偶数个元素时取中间两个的平均还是之类的细节。
我把这些陷阱对应到具体处理方式,做成一张速查表:
| 陷阱类型 | 触发条件 | 应对方式 |
|---|---|---|
| 整数溢出 | 求和、乘积、大数相乘 | 改用 long long |
| 越界访问 | 访问相邻元素 a[i+1] | 循环边界收紧或特判 n=1 |
| 并列规则 | 多个答案并列 | 明确二级比较条件 |
| 精度问题 | 浮点比较、输出 | 用浮点或规避除法 |
5.2 交卷前的自查清单
我现在每次写完第一题,都会花两分钟跑一遍这个自查流程,基本能挡掉百分之九十的失误。
首先确认数据类型:涉及累加、乘积的地方是不是都用了足够大的类型。然后检查循环边界,尤其是所有访问相邻下标的循环,n=1 的极端情况会不会崩。接着核对输出格式,行尾有没有多余空格,大小写对不对。最后拿题目给的样例手跑一遍,再自己构造一两个边界用例,比如全相同的数据、只有一项的数据、最大值的数据。
这两分钟很值。第一题的代码通常很短,改起来快,但如果你交卷之后才发现问题,那道题就白写了。
6. 上场时的时间分配与调试策略
前面讲的都是技术和细节,最后聊点实战节奏。第一题虽然简单,但它在整场考试里的作用是稳定军心,节奏乱了比写不出来更麻烦。
我的建议是,开考后先花三到五分钟把五道题扫一遍,搞清楚每道题大概考什么。这一遍不深读,就是建立全局感,避免出现"死磕第三题结果第一题简单到送分却没看到"的尴尬。
然后从前到后做。第一题给自己定个上限,比如二十分钟,正常十分钟内就该写完。写完立刻编译、跑样例,样例过了之后再想两个边界用例。如果二十分钟还没搞定,先跳过去做后面的,回头再回来——很多时候你冷静一下,回头一看就知道错在哪了。
调试第一题有个小技巧:先把中间状态打出来。第一题代码短,加几行临时输出看关键变量的值,比盯着代码干想快得多。尤其是模拟题,打印每一轮的状态变化,一眼就能看出哪一步更新错了。调完记得把调试输出删掉,别留在最终提交的代码里。
还有一点关于语言选择。CSP 支持 C++、Java、Python 等,第一题用什么都能过。但如果你平时习惯 C++,就老老实实用 C++,别临场换 Python 图快——第一题本身没难度,换语言带来的读入、输出、环境不熟悉的风险反而更大。我见过有人因为不熟 Python 的输入处理,在第一题上多花了一倍时间。
说到底,CSP 第一题的难度就摆在那里,它真正筛选的不是谁的算法厉害,而是谁够细心、谁的工程习惯够扎实。把题型归类记熟,把读入输出和边界条件的坑一个个填平,再把上场节奏安排明白,这 100 分就是稳稳到手的。我自己从最开始"第一题也要调半小时"到现在"十分钟内必交",中间没有捷径,就是把历年第一题反复刷、反复总结错在哪。你现在看到的这份汇总,就是这些年记录里最有用的那部分,希望能帮你在考场上少走点弯路。