CCF CSP第一题满分攻略:五大题型、代码模板与避坑指南
2026/9/18 23:07:45 网站建设 项目流程

第一次系统性地把 CCF CSP 的第一题全部翻出来重做、归类、做笔记,起因特别朴素:我发现身边不少同学卡在第一题上,不是算法不会,而是读题慢、输入输出写崩、边界没兜住,明明是全卷最容易拿满的 100 分,却在这里丢分,然后心态跟着炸,后面四题一起废。所以我把这套卷子里最"送分"的部分单独拎出来,做一份 ccf_csp 第一题汇总,把命题规律、题型分类、代码模板和踩过的坑全写清楚。

先说清楚这份汇总的定位。CSP 认证通常一场五道题、满分 500 分,考试时间大概四小时,题目难度从入门一路爬到需要图论和动态规划的硬骨头。第一题在评分体系里就是那 100 分的"地基题",绝大多数情况下只需要数组、循环、简单数学和字符串处理就能解决,代码量往往在三十行以内。它适合所有准备认证的人——不管你是刚学完 C++ 语法的大一新生,还是工作几年想刷个认证充实简历的开发者,第一题都是必须拿下的。这篇内容我不打算给你灌鸡汤,只讲三件事:这类题怎么分类、代码怎么写最稳、哪些坑一定会踩。

1. 第一题到底在考什么

很多人把第一题理解成"送分题",于是做题的时候随手写、随手交,结果分数出来发现只拿了 60 分、80 分,回头一看是被特殊数据卡掉了。要真正吃透第一题,得先搞清楚命题人在这里到底想考你什么。

1.1 评分机制决定了第一题的性价比

CSP 的评测是分数制,每道题按测试点给分,不是全对才有分。这意味着第一题的策略和其他题完全不同:其他题你可能冲一部分分就收手,但第一题必须冲满分,因为它的测试点几乎都是同一种逻辑,只是数据规模不同,拿不到满分说明代码里有系统性缺陷,而不是"没想到某个高深算法"。

从时间投入产出比来看,第一题理论上十五分钟内必须解决。我自己的节奏是:读题三分钟,写代码八分钟,调试和自测四分钟。如果你在第一题上花了半小时还在调,那说明你的问题不在这一题,而在基础的输入输出和边界处理上,这个必须补。

还有一个容易被忽略的点:第一题的正确率会直接影响后面的心态。我见过太多人第一题反复提交失败,越交越急,结果第二题连题都没读进去。所以第一题的目标不只是那 100 分,更是把节奏稳住。

1.2 从历年题目看命题人的思路

我把能收集到的第一题列了一遍,能明显看出命题有一条稳定的主线:用最直白的生活场景包装最简单的数据处理。比如"打酱油"、"跳一跳"、"卖菜"、"小明上学"、"田地丈量"这些题,场景全是日常化的,但剥掉包装之后,核心无非是几个动作:读一批数、做一次遍历、算一个结果、按格式输出。

再看数据规模,第一题的测试数据通常在几千到几十万这个量级,几乎不需要考虑时间复杂度优化,一个 O(n) 或 O(n log n) 的朴素写法就能过。命题人从来没打算在第一题上为难你,他在意的是你能不能把题面读准、把格式对死、把边界收干净。

下面这张表是我整理的历年第一题的典型样本,你可以对着它感受一下命题的重复度:

题号题目名称核心考点
201312-1出现次数最多的数哈希计数、最值比较
201403-1相反数集合查找
201409-1相邻数对排序后相邻比较
201412-1门禁系统频次累加
201503-1图像旋转二维数组下标变换
201509-1数列分段遍历计数
201512-1数位之和整数拆位
201604-1折点计数相邻三点比较
201609-1最大波动相邻差值
201703-1分蛋糕累加与断点
201709-1打酱油贪心枚举
201803-1跳一跳连续状态累加
201809-1卖菜邻域平均
201812-1小明上学分段模拟
201903-1小中大中位数与格式控制
202006-1线性分类器点与直线位置判断
202104-1灰度直方图矩阵频次统计
202109-1数组推导前缀最值反推
202112-1序列查询分段函数求和
202206-1归一化处理均值方差公式
202212-1现值计算幂运算与浮点
202303-1田地丈量矩形交集面积
202305-1重复局面状态去重
202403-1词频统计字符串计数

看着挺杂,其实就五个套路。这份表格我建议你打印出来贴在屏幕边上,刷题的时候对着归类,比漫无目的地刷要高效得多。

2. 把第一题拆成五类题型

刷题最怕的就是把每一题都当成新题。第一题的题型重复率极高,只要你把分类做出来,遇到新题的第一步就变成了"这题属于哪一类",然后直接套对应的思路和模板。我把它分成五类,每一类都给你讲清楚识别特征和通用打法。

2.1 计数统计型:出现次数最多、频次、去重

这一类是第一题里出现频率最高的,识别特征非常明显:题目会说"统计每个 X 出现的次数",或者"找出出现次数最多的"。

典型代表是"出现次数最多的数"(201312-1):给 n 个正整数,找出出现次数最多的那个数,如果有多个并列,输出最小的那个。这题的坑点在"并列取最小",很多人只用 max 记最大次数,忘了处理并列时取最小值的约束。

通用的解法就是开一个计数结构。C++ 里用 map 或者数组都行,用 map 的好处是它默认按 key 升序排列,遍历的时候遇到第一个最大次数就是答案,天然满足"并列取最小"。这个细节说出来很轻,但真到考场上,能省下你三分钟的纠结:

#include <bits/stdc++.h> using namespace std; int main() { int n; scanf("%d", &n); map<int, int> cnt; for (int i = 0; i < n; i++) { int x; scanf("%d", &x); cnt[x]++; } int best = -1, ans = -1; for (auto &p : cnt) { if (p.second > best) { best = p.second; ans = p.first; } } printf("%d\n", ans); return 0; }

注意这里用的是严格大于>而不是大于等于,正是因为 map 已经升序,只要用严格大于,遇到并列就不会覆盖,留下来的一定是最小的那个。这种"用数据结构的天然性质省掉额外判断"的思路,在第一题里特别值钱。

同类的还有"门禁系统"(201412-1),记录每个编号出现的次数,输出"这是第几次出现";"相反数"(201403-1)用集合判断,输入里成对出现的相反数,统计对数,值域小的时候直接开数组标记比 set 更快。

2.2 纯模拟型:按规则一步步走

模拟型题目的画风是"给你一套规则,让你照着模拟出一个过程,最后输出结果"。它的难度不在算法,而在"你有没有把规则读全"。

"跳一跳"(201803-1)就是经典。规则是:跳到方块上得 1 分,跳到中心得 2 分,如果连续跳到中心,第 k 次连续得 2k 分,输入以 0 结束。这题的核心变量只有一个——连续次数:

#include <bits/stdc++.h> using namespace std; int main() { int x, score = 0, combo = 0; while (scanf("%d", &x) == 1 && x != 0) { if (x == 1) { score += 1; combo = 0; } else { combo++; score += 2 * combo; } } printf("%d\n", score); return 0; }

这里有两个细节值得说。第一,scanf的返回值判断要写对,== 1才是成功读入一个数,很多人写成!= EOF,在混合输入时会出问题。第二,读到 1 的时候必须把combo清零,这是"连续状态"类题目的通用动作,一旦断了就要归零,忘了归零会多算分。我当年第一次做这题就是漏了清零,样例过了,提交 80 分,查了半天才反应过来。

"小明上学"(201812-1)也是模拟,但它考的是分段处理:红灯等待、黄灯等待、绿灯通过,还有上学和放学两种方向,规则多但每条都直白。做这类题我习惯先把所有规则用注释列在代码开头,写一条勾一条,避免漏掉。

2.3 数组与矩阵操作型:下标变换是重灾区

矩阵类是很多人第一题翻车的地方,因为它涉及二维下标的重新映射,稍不注意就转错方向。

"图像旋转"(201503-1)要求把 n 行 m 列的矩阵逆时针旋转 90 度。记住一个通用推导:原矩阵 A 的第 i 行第 j 列,旋转后落到新矩阵 B 的第m-1-j行第i列。有了这个映射,代码就是三行嵌套循环:

#include <bits/stdc++.h> using namespace std; int a[1005][1005], b[1005][1005]; int main() { int n, m; scanf("%d %d", &n, &m); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) scanf("%d", &a[i][j]); for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) b[m - 1 - j][i] = a[i][j]; for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { printf("%d", b[r][c]); printf(c == n - 1 ? "\n" : " "); } } return 0; }

这段代码有三个值得抄的地方。第一,矩阵开成全局数组,避免大数组放在栈上导致爆栈,n 和 m 上千的时候这个区别很致命。第二,输出时用c == n-1 ? "\n" : " "控制行末不留多余空格,CSP 的评测对格式敏感,行末空格有时候会判错。第三,映射公式我会在草稿纸上先用一个小例子验证一遍,拿 2x3 的矩阵手推,两分钟的事,能省掉十分钟的调试。

"灰度直方图"(202104-1)考的是矩阵遍历统计,把所有灰度值数一遍,输出每个灰度级出现的次数。这种题的要点是搞清楚灰度值的范围,直接开一个对应大小的数组做桶,遍历矩阵自增即可,比 map 快得多。

2.4 公式推导与数学型:浮点和精度是雷区

数学型题目的特征是题面给一个公式,让你照着算。看起来最简单,实际上是最容易在格式和精度上丢分的一类。

"归一化处理"(202206-1)要求先算均值和方差,再把每个数标准化。公式不复杂,但有两个坑:方差是除以 n 不是除以 n-1,以及输出需要保留足够的小数位:

#include <bits/stdc++.h> using namespace std; int main() { int n; scanf("%d", &n); vector<double> a(n); double sum = 0; for (auto &x : a) { scanf("%lf", &x); sum += x; } double mean = sum / n, var = 0; for (double x : a) var += (x - mean) * (x - mean); var /= n; double sd = sqrt(var); for (double x : a) printf("%.16f\n", (x - mean) / sd); return 0; }

这里的%.16f是我个人的习惯。CSP 对浮点输出的判定通常要求误差小于某个阈值,输出位数给足,误差就越小,被判定为"格式不符"的风险也越低。如果你用cout,记得配setprecision(16),默认的六位有效数字经常不够。

"现值计算"(202212-1)是另一类,题目给年利率和每年的现金流,求现值之和。核心就是复利公式,每一项除以(1+r)的 i 次方。这题的坑在于不要中途四舍五入,全程用 double 累加,最后一次性输出:

double ans = 0; for (int i = 1; i <= n; i++) { ans += a[i] / pow(1 + r, i); } printf("%.4f\n", ans);

"数组推导"(202109-1)稍微绕一点,给你前缀最大值数组,反推原数组和的最大值和最小值。最大值直接就是所有前缀最大值之和,最小值是所有"新出现的最大值"之和。这种题的解法是先想清楚逻辑,再动手写代码,千万别一上手就写循环。

2.5 字符串与格式控制型:字符处理最考验耐心

字符串类在第一题里出现得不算多,但一出现就容易因为细节丢分。"词频统计"(202403-1)这类题目一般要求对给定的字符串做分词或者计数,你需要考虑分隔符、大小写、空串这些情况。

格式控制型里最经典的当属"小中大"(201903-1)。题目给一串数,要求输出最大值、中位数、最小值,并且规定:如果是整数就直接输出整数,如果是小数就保留一位小数,顺序是先大后小。这题我见过太多人栽在中位数的格式上:

#include <bits/stdc++.h> using namespace std; int main() { int n; scanf("%d", &n); vector<long long> a(n); for (auto &x : a) scanf("%lld", &x); sort(a.begin(), a.end()); printf("%lld ", a.back()); if (n % 2 == 1) { printf("%lld ", a[n / 2]); } else { long long s = a[n / 2 - 1] + a[n / 2]; if (s % 2 == 0) printf("%lld ", s / 2); else printf("%.1f ", s / 2.0); } printf("%lld\n", a.front()); return 0; }

注意偶数个数据时中位数的处理:两个中间值相加是偶数就输出整数,是奇数才输出一位小数。很多人图省事直接写printf("%.1f"),结果整数情况输出了3.0,评测判错。这种题的关键是用整数运算处理整数情况,只在必须输出小数时才引入浮点,这样精度和格式都稳。

3. 输入输出的那些坑

我统计过自己第一题丢分的原因,排第一的不是算法错,是输入输出。听上去很反直觉,但这就是现实。

3.1 C++ 选手的稳妥写法

CSP 的输入格式有时候并不规整,同一组数据可能全部在一行,也可能被拆成好几行。用cin>>的好处是它会自动跳过所有空白字符,不管是空格还是换行,你都能正确读到一个数。用scanf也一样,%d会跳过前导空白。

所以第一题的读入完全可以写成"一个 for 循环读 n 个数",不需要关心它们在几行里:

int n; scanf("%d", &n); for (int i = 0; i < n; i++) { int x; scanf("%d", &x); // 处理 x }

如果追求速度,可以在 main 开头加一句ios::sync_with_stdio(false); cin.tie(nullptr);,让cin接近scanf的性能。不过我得说句实话:第一题的数据量根本用不上这个优化,它更多是心理安慰。真正需要担心性能的是第四第五题。

3.2 Python 选手必须改掉的习惯

用 Python 考 CSP 的人越来越多,但 Python 在输入上有几个天然劣势。最常见的错误是逐行input()

n = int(input()) a = list(map(int, input().split()))

这段代码在一行就是一个数据的时候没问题,但一旦数据多行排布,第二行就读不全,直接报错或者读少数据。稳妥的写法是一次性读完全部输入再切片:

import sys def main(): data = sys.stdin.read().split() idx = 0 n = int(data[idx]); idx += 1 a = [] for _ in range(n): a.append(int(data[idx])); idx += 1 # 后续处理 main()

这样不管输入怎么换行,都能按顺序取到所有 token。另一个 Python 的坑是递归深度和循环性能,第一题基本不涉及递归,但循环次数上百万的时候,PyPy 和 CPython 的差距会很明显,能用列表推导式就别写显式 for。

3.3 输出格式的死规矩

输出格式这块,我总结出三条死规矩。第一,每行末尾不留多余空格。第二,最后一行必须有换行符,很多评测器认这个。第三,浮点数位数给足,不要为了好看缩减。

行末空格看着是小事,但有些评测在做字符串比对时是逐字符的,末尾多一个空格就整行判错。我的习惯是用一个判断决定分隔符:

for (int i = 0; i < n; i++) { printf("%d", a[i]); printf(i == n - 1 ? "\n" : " "); }

这套写法我在所有第一题里都用,从来没出过格式问题。

4. 逐题实战拆解

前面讲的是方法,这一节我挑几道有代表性的题,把完整思路和代码走一遍,你可以直接拿来当模板。

4.1 卖菜:邻域平均的边界处理

"卖菜"(201809-1)的规则是:第一天每家菜价是a[i],第二天每家的价格是自己和左右邻居三家的平均值,向下取整。两端的店只有两家参与平均。

这题的难点全在边界。我的写法是用一个计数器记录参与平均的店数,避免在两端写重复代码:

#include <bits/stdc++.h> using namespace std; int main() { int n; scanf("%d", &n); vector<int> a(n); for (auto &x : a) scanf("%d", &x); for (int i = 0; i < n; i++) { int sum = a[i], cnt = 1; if (i > 0) { sum += a[i - 1]; cnt++; } if (i < n - 1) { sum += a[i + 1]; cnt++; } printf("%d", sum / cnt); printf(i == n - 1 ? "\n" : " "); } return 0; }

if判断边界而不是把数组开大在外围补一圈 0,原因是补 0 会把两端的平均值拉低,逻辑上就错了。向下取整直接用整数除法,C++ 对正数就是截断,正好符合要求。

4.2 线性分类器:点在直线哪一侧

"线性分类器"(202006-1)给一堆点和一条直线,判断所有 A 类点是否在直线同侧、B 类点是否在另一侧。核心就是代入直线方程看符号。

#include <bits/stdc++.h> using namespace std; int main() { int n, m; scanf("%d %d", &n, &m); vector<int> x(n), y(n); vector<char> type(n); for (int i = 0; i < n; i++) { scanf("%d %d %c", &x[i], &y[i], &type[i]); } while (m--) { int t0, t1, t2; scanf("%d %d %d", &t0, &t1, &t2); int ca = 0, cb = 0; bool ok = true; for (int i = 0; i < n; i++) { long long v = (long long)t0 + (long long)t1 * x[i] + (long long)t2 * y[i]; int side = (v > 0) ? 1 : -1; if (type[i] == 'A') { if (ca == 0) ca = side; else if (ca != side) ok = false; } else { if (cb == 0) cb = side; else if (cb != side) ok = false; } } if (ca == cb) ok = false; printf(ok ? "Yes\n" : "No\n"); } return 0; }

这题有两个隐藏坑。第一,坐标和系数相乘可能溢出 int,必须用long long,这是典型的"数据规模藏在题面角落"的陷阱。第二,最后还要检查两类点是不是真的在两侧,只判断各自同类还不够,ca == cb说明两类跑到同一侧去了。我第一遍写的时候就是漏了这一步,样例过了但被特殊数据卡掉。

4.3 田地丈量:矩形交集的统一公式

"田地丈量"(202303-1)求两个矩形的交集面积。这类题的通用公式是:交集宽度等于min(右边界) - max(左边界),高度同理,如果出现负数说明没有交集,面积取 0。

long long w = min(r1, r2) - max(l1, l2); long long h = min(t1, t2) - max(b1, b2); long long area = (w > 0 && h > 0) ? w * h : 0;

这个公式不只适用于第一题,很多人脸识别、目标检测里的 IOU 计算用的也是它。记住一次,一辈子受用。需要注意的是矩形坐标的表示方式,题目里可能是"左下右上",也可能用其他组合,一定要先把坐标理顺再套公式。

5. 高频易错点速查

做了这么多题,我把反复踩到的坑整理成一张表,你现在就可以对着检查自己的代码。

易错点典型表现修正方式
数据类型溢出坐标或系数相乘超 int提前用 long long
浮点精度不足输出位数太少判错保留 10 位以上小数
行末多余空格格式比对失败用条件判断控制分隔符
连续状态未清零跳一跳多算分状态断裂时重置变量
边界未单独处理数组下标越界用计数器法避开边界分支
并列条件漏判取最值时未处理并列结合数据结构的排序性质
读入方式不兼容多行输入只读一行用 token 流统一读入
输出缺少换行最后一行判错末尾统一补\n

再说几个表格装不下的经验。第一,样例一定全过不代表能拿满分,CSP 的测试点里专门有边界数据,比如 n=1、全部元素相同、极端值。第二,提交前用自己造的边界数据跑一遍,这一步能拦住至少一半的意外丢分。第三,不要把第一题写得太"聪明",不需要用高级数据结构的地方就别用,代码越短越容易验证正确性。

我在排查的时候习惯用"三遍法":第一遍对着样例手算,确认逻辑;第二遍用极端数据测,比如 n 取 1 和取最大值;第三遍把自己的代码读一遍,专门找有没有忘记初始化、忘记清零的变量。这个流程走下来,第一题基本不会翻车。

6. 训练路线与刷题方法

最后聊聊怎么练。我见过两种极端:一种是把所有第一题刷一遍但每道都只做一遍,另一种是死磕某几道难题。两种效率都不高。

6.1 按题型分组刷,别按题号顺序刷

题号顺序是时间顺序,不是难度顺序,也不代表考点相似。正确的刷法是先按我前面分的五类建五个文件夹,把题目归进去,然后一类一类地刷。同一类的题连着做,你会发现它们的骨架几乎一样,做第三题的时候就不需要思考了,直接条件反射写出模板。

第一遍刷的时候,每道题都要求自己写完整代码并提交,不要看题解。卡住了先自己调,超过二十分钟再去看别人的思路,看完之后合上答案重新写一遍。这个"合上答案重写"的动作很关键,它能把别人的思路真正变成你的肌肉记忆。

6.2 建自己的模板库

刷到一定程度,你会发现自己反复写同样的代码。这时候就该建模板库了,把常用的片段整理成文件,比如:

// 通用读入 n 个数到 vector int n; scanf("%d", &n); vector<int> a(n); for (auto &x : a) scanf("%d", &x); // 带格式控制的数组输出 for (int i = 0; i < n; i++) { printf("%d", a[i]); printf(i == n - 1 ? "\n" : " "); } // 二维矩阵读入 for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) scanf("%d", &g[i][j]);

模板库的意义不是让你抄,而是让你在考场上少写几行样板代码,把注意力留给逻辑本身。我自己的模板库到现在还留着,偶尔接算法外包的时候也在用。

6.3 时间分配上的一点体会

我个人的节奏是:前十五分钟必须结束第一题,哪怕代码看起来还能再优化也不管,先交上去拿分。第一题多花的时间都是从第四第五题身上抢的,而那两道题才是真正拉开差距的地方。

另外说一个心态上的经验。第一题交上去之后不要反复回看,直接翻下一页。我见过有人第一题拿了 100 分还回头检查了三遍,结果第三题没时间做。这种时间浪费是最可惜的。

关于参考资料,CCF 官方题库里的历年真题是最权威的,配上任意一本讲算法入门的书就够了。不用买太多资料,把五类题型吃透,比刷完十本书都管用。

我在实际操作中发现,第一题这个东西,练到后期会形成一种"看到题面就知道要写什么"的直觉。这种直觉不是天赋,是分类加重复的结果。你把这五类题型各做上七八道,再回来做新题,就会发现命题人其实一直在一个很小的圈子里打转。真正需要你警惕的,永远是那些看起来太简单、让你想跳过自测环节的题目——它们往往就是那个会让你丢分的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询