CSP-J/S 复赛与初赛最大的不同,是「会做」不等于「得分」。每年复赛结束后,总有大量选手对答案时发现:算法想对了,代码也写出来了,分数却是 0 或者远低于预期。复盘这些案例会发现,真正导致爆零的往往不是知识盲区,而是一类完全可以提前规避的低级失误。
本文把复赛中最常见的 10 个丢分点逐一拆开,每个坑都给出出错场景和对应的规避方法。建议考前把文末的最终检查清单过一遍。
坑 1:文件读写没写 —— 最经典的整题爆零
CSP 复赛的评测方式和平时在洛谷刷题完全不同:评测系统从 xxx.in 文件读入数据,把结果写进 xxx.out 文件。如果程序里没有 freopen,程序会一直等键盘输入,评测系统等不到输出,直接判 0 分。
正确写法(文件名以题目要求为准):
#include <iostream>
using namespace std;
int main() {
freopen("problem.in", "r", stdin); // 从文件读入
freopen("problem.out", "w", stdout); // 输出到文件
// 你的代码
return 0;
}
需要逐一确认的三点:
| 检查项 | 说明 |
|---|---|
| 文件名 | 与题目要求完全一致,注意大小写和拼写 |
| 扩展名 | .in / .out 是否与题目要求一致,不同年份可能有差异 |
| 提交前复查 | 最后几分钟专门检查每道题的文件读写 |
一句话总结:宁可不写算法,不能不写 freopen。写了可能拿分,不写一定 0 分。
坑 2:该用 long long 的地方用了 int
典型场景:题目说 n ≤ 10^9,或者答案要对 10^9+7 取模,选手用 int 存中间结果,一次乘法就溢出,结果变成负数或错误值。
判断要不要开 long long,看这几条:
| 情况 | 例子 | 建议 |
|---|---|---|
| 数据范围接近或超过 2×10^9 | n ≤ 10^9 |
用 long long |
| 涉及乘法 | a×b,其中 a、b 都可能 ≥ 10^5 | 用 long long |
| 前缀和、累加和 | n 个数求和,每个数 ≤ 10^9 | 用 long long |
| DP 状态值可能很大 | dp[i] 可能 ≥ 2^31 |
用 long long |
| 拿不准的时候 | — | 直接上 long long |
三个容易漏改的位置:
- 定义变量时看到 10^9 级别数据,条件反射写 long long;
- 函数返回值也要改成 long long,不能只改变量类型不改返回类型;
const int INF = 1e18;这种写法本身就是错的,应写成const long long INF = 1e18;。
坑 3:数组开太小 —— RE 了都不知道为什么
典型错误:
int a[100000]; // 题目说 n ≤ 100000
如果下标从 1 开始用 a[1]~a[n],看起来刚好够。但只要在末尾加了哨兵、或用到了 a[n+1],数组立刻越界,程序 RE,整题 0 分。
正确做法是多开几个「保险位」:
const int N = 100000 + 5; // 多开 5~10 个
int a[N];
各常见场景的推荐开法:
| 场景 | 建议大小 |
|---|---|
| 存 n 个数 | n + 5 |
| 邻接表存无向图 | (m + 5) × 2 |
| DP 多开一维 | (n+1) × (m+1) |
| 字符串 | s.length() + 5 |
坑 4:边界条件没处理 —— n=0、n=1 必挂
n=0 或 n=1 这类小数据没有单独处理时,常见后果是数组越界、死循环或输出错误。需要重点自查的边界情况:
| 情况 | 容易出问题的地方 |
|---|---|
| n=0 | 循环 i < n 不执行,但后面是否还用到了 a[0]? |
| n=1 | 图论题的链、树的直径、DP 初始化 |
| 最值问题 | 初始值设成 0 还是设成 INF / -INF? |
| 空输入 | 字符串为空、数组为空时程序会不会崩? |
| 负值 | 坐标、差值、模运算的结果会不会是负数? |
建议每道题写完后,手动构造 n=0、n=1 的边界数据跑一遍;DP 题的初值和边界转移单独写注释标出来;二分查找的 left/right 初值也要想清楚。
坑 5:循环边界写错 —— 多跑一次或少跑一次
经典错误一:遍历范围写多:
for (int i = 0; i <= n; i++) // 应该是 i < n
本来只需遍历 a[0]~a[n-1],多跑一次 a[n] 直接越界。
经典错误二:二维数组行列长度不同,却用同一个变量控制:
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) // 应该是 j < m
四种常见循环写法对照:
| 写法 | 遍历范围 | 注意点 |
|---|---|---|
for(i=0; i<n; i++) |
[0, n-1] | 最常见,下标从 0 开始 |
for(i=1; i<=n; i++) |
[1, n] | 数组要开 n+1 以上 |
for(i=n; i>=1; i--) |
[n, 1] | 倒序,注意是 >= |
for(i=n-1; i>=0; i--) |
[n-1, 0] | 下标从 0 开始时的倒序 |
写循环前先确定下标从 0 还是 1 开始;写完后手动代入 i=0、i=n-1、i=n 各走一遍。
坑 6:调试输出忘了删 —— 答案被淹没
调试时加了一堆中间输出,提交前忘删:
cout << "debug: " << x << endl;
评测输出变成:
debug: 5
debug: 10
25
正确答案是 25,但前面的调试信息会被判定为格式错误,整题 0 分。
规避方法:
- 提交前全局搜索
cout、printf、cerr,确认没有调试输出; - 平时习惯用
cerr输出调试信息——它不写进 stdout,即使忘删也不影响评测输出; - 或者用条件编译,正式提交时定义宏关闭:
#ifdef DEBUG
cout << "debug: " << x << endl;
#endif
- 养成习惯:结束前几分钟只做两件事——查文件读写、查调试输出。
坑 7:变量没初始化 —— 随机值引发的血案
int sum, cnt; // 未初始化
for (int i = 0; i < n; i++) {
if (a[i] > 0) sum += a[i];
cnt++;
}
sum 和 cnt 没赋初值。n=0 时 cnt 是随机数;就算 n>0,sum 也是从一个随机值开始累加,结果完全不可预测。
各类变量的推荐初值:
| 变量类型 | 初始值 | 用途 |
|---|---|---|
| 累加器 sum | 0 | 求和、计数 |
| 最大值 max | -INF 或 a[0] | 找最大值 |
| 最小值 min | +INF 或 a[0] | 找最小值 |
| 计数器 cnt | 0 | 计数 |
| 标志位 flag | false / 0 | 状态判断 |
| DP 数组 | 0 或 INF | 动态规划 |
注意:多组测试数据时,每组开始前必须重新初始化。全局变量虽然默认清零,但不要依赖这个特性,显式赋值更安全。
坑 8:递归爆栈 —— 深度太大程序崩
用 DFS 遍历一棵 n=100000 的链状树,递归深度达到 10^5,系统栈空间有限,递归几千层就崩了:
void dfs(int u) {
for (int v : g[u]) {
dfs(v); // n=100000 时直接爆栈
}
}
可选的解决方案:
| 方案 | 说明 |
|---|---|
| 改 BFS / 迭代 | 用队列做 BFS,或用栈模拟递归(首选) |
| 手动模拟栈 | 用 STL 的 stack 替代函数递归 |
| 增加栈空间 | 比赛环境不可控,不要依赖 |
| 尾递归优化 | C++ 编译器不保证支持,不要依赖 |
容易爆栈的场景:链状树的深度遍历、n ≥ 10^5 的递归回溯、最坏情况下递归深度 O(n) 的快排。
经验法则:递归深度超过 10^4 就要警惕,超过 10^5 必须改迭代。
坑 9:输入格式没看清 —— 空格换行读错
两种典型场景:
- 题目说「第一行两个整数」「接下来 n 行每行 m 个整数」,实际数据中某行有前导空格或分隔符不同,按固定格式读就直接读乱;
- 用
cin >> s读字符串,但题目要求的是包含空格的整行文本——此时应该用getline(cin, s)。
不同输入类型的正确读法:
| 输入类型 | 正确读法 |
|---|---|
| 整数 / 浮点数 | cin >> x 或 scanf |
| 无空格单词 | cin >> s |
| 包含空格的整行 | getline(cin, s)(与 cin 混用时先 cin.ignore()) |
| 单个字符(可能含空格) | scanf(" %c", &c)(加空格跳过空白) |
| 分隔符不确定 | 按字符串整行读入后再手动拆分 |
原则:先看清输入格式再写读入代码;不确定时整行读入再解析。
坑 10:不读题直接写 —— 最可惜的丢分
看个大概觉得「这题我做过」,直接开写,一小时后样例过了,交上去 0 分——回头读题才发现,题目给的约束条件和自己的理解完全不同。
推荐的读题流程:
| 步骤 | 时间 | 做什么 |
|---|---|---|
| 第一遍:快速浏览 | 1 分钟 | 看题目大意、输入输出格式 |
| 第二遍:细读 | 4 分钟 | 逐字读题,圈出关键条件、数据范围、特殊要求 |
| 第三遍:确认 | 2 分钟 | 用自己的话复述题意,对照样例验证 |
| 构思 | 5 分钟 | 想算法、估算复杂度、考虑边界 |
算一笔账:读题 10 分钟 + 写题 30 分钟 = 40 分钟,一次做对;不读题直接写 10 分钟写错,再花 30 分钟改,同样 40 分钟,还未必改得对。时间成本一样,成功率天差地别。
考场最终检查清单
提交前 5 分钟,对每道题过一遍:
- [ ] freopen 写了吗?文件名对吗?
- [ ] 调试输出删了吗?(cout / printf 全局搜一遍)
- [ ] long long 用对了吗?所有乘法都考虑了吗?
- [ ] 数组够大吗?多开 5 个了吗?
- [ ] 所有变量都初始化了吗?多组数据重新初始化了吗?
- [ ] n=0、n=1 的边界数据测了吗?
- [ ] 循环边界写对了吗?(
<还是<=?) - [ ] 递归深度会爆栈吗?要不要改成迭代?
做题过程中的好习惯:
- 读完题用自己的话复述一遍题意;
- 先看数据范围,再决定 int 还是 long long;
- 核心逻辑写完,手动跑一遍小样例;
- 注释写给自己看,标记边界和易错点;
- 主动测边界数据(n=0、n=1、最大最小值)。
写在最后
这 10 个坑,每一个都是复赛考场上真实发生过的丢分案例。拿过一等奖的选手不是从不踩坑,而是踩过一次就不再踩第二次。
复赛比的不是谁会的算法多,而是谁在有限的 3.5 小时里犯的错更少。从现在开始,每次模拟赛都按清单检查,把检查变成肌肉记忆,考场上才不会掉链子。
本文整理自公众号「开心算法」文章《CSP复赛十大丢分重灾区——从文件读写到边界条件》(2026-09-23 发布,文 / 开心算法 · 信息学奥赛避坑笔记),内容经重新组织与补充,原文链接:https://mp.weixin.qq.com/s/HuAq66gXDmAPTA8fC7dFNQ 。仅供学习交流,具体比赛规则以 CCF 官方公布为准。


