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

三个容易漏改的位置:

  1. 定义变量时看到 10^9 级别数据,条件反射写 long long;
  2. 函数返回值也要改成 long long,不能只改变量类型不改返回类型;
  3. 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 分。

规避方法:

  1. 提交前全局搜索 cout、printf、cerr,确认没有调试输出;
  2. 平时习惯用 cerr 输出调试信息——它不写进 stdout,即使忘删也不影响评测输出;
  3. 或者用条件编译,正式提交时定义宏关闭:
#ifdef DEBUG
    cout << "debug: " << x << endl;
#endif
  1. 养成习惯:结束前几分钟只做两件事——查文件读写、查调试输出。

坑 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:输入格式没看清 —— 空格换行读错

两种典型场景:

  1. 题目说「第一行两个整数」「接下来 n 行每行 m 个整数」,实际数据中某行有前导空格或分隔符不同,按固定格式读就直接读乱;
  2. 用 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 小时里犯的错更少。从现在开始,每次模拟赛都按清单检查,把检查变成肌肉记忆,考场上才不会掉链子。