记录一下 CSP-S 2025 到来之前,我打过的模拟赛们。
核桃编程模拟赛
过程记录
这是我打的第一场对标 CSP-S 难度的 OI 赛制比赛,并且我全程使用的 NOI Linux 2.0。
整场下来的经过:
- 没有中文输入法,没法注释做题了,用草稿纸做题好不习惯。
- 使用的 Sublime 写的代码,挺好用哈。
- 四个题全都看了一遍,分析出了总共 100 分左右的暴力分,然后开始打,这个时候大概过去了 25 分钟。
- 直接开写第一题的第二档暴力,咋感觉不太好写。。。算了还是写一下最暴力的做法吧。
- 第二题暴力打完,哎还有一个性质极为简单,直接输出答案即可。
- 第三题打一个最暴力的做法,然后发现有两个性质非常好做,顺手写了。
- 第四题写了个最暴力的做法。
- 咋写了这么多不同的部分分代码?3 小时输出了快 600 行代码。
- 为了测不同的部分分,开始疯狂改
solve()
函数里的分派逻辑,改得乱七八糟。 - 我的代码疑似变得非常乱了,不同部分分之间的数据结构以及函数堆在了一起难以区分。看起来还是学一下咋用
namespace
比较好,每个部分分写到一个namespace
里面。 - 还剩 1 小时,我是写 T1 另外 40 分的部分分,还是写 T2 20 分部分分?我 T1 40 分的代码已经有了,要不继续写 T1 吧。
- 好难写的 T1,终于写好了,写个拍子试一下。
- 对拍挂了,怎么看起来是暴力挂了?哎真的是暴力挂了吗?哦原来还是部分分写错了。
- 手动测一下,SF 了,看来得二分调试法了。
- 全程
cout
调试,然后发现删调试好麻烦啊,ctrl + f 到处看防止漏删爆零!后来想到定义一个LOCAL_DEBUG
宏,把调试语句都用这个宏包起来,本地测试时带上这个宏,提交时评测机上没有这个宏。 - 还剩 10 分钟,感觉调不出来了,不改任何逻辑了,看一下每个题的
solve()
函数,然后直接交。 - 我超忘了加文件读写了,还好 OJ 有提示,后来想起来可以加
LOCAL_TEST
宏,如果没有定义(#ifndef LOCAL_TEST
)这个宏,则条件编译文件读写代码。 - 最后由于全都打的暴力,所以甚至大样例都没有用上,后面几个题也约等于没有做测试,理论上最高可能得分 125 分,但是感觉可能会挂分。
- OI 赛制和其他赛制差别太大了,要考虑的其他因素好多,非智力精力消耗很大,感觉因为这些原因少写 30 分的部分分,一直在打打打打暴力,没有做很多深入的思考。
经验教训
不要预先假定你的 T1 能拿满分,甚至不要预先假定能拿 50 分。最开始就从最基本的暴力开始打就好,反正最后也是要对拍的。
一等奖分数线并不高,稳妥起见,建议永远先打更容易写的部分分。这次比赛时,最后一个多小时就遇到了 40 分难写和 10 分相对好写的部分分之间的抉择,决策时考虑到 40 分的部分分的代码最最开始已经写过一些了,感觉更容易快速写完,于是就选的 40 分的,结果最后对拍发现寄了,且没调出来。
大样例只能测正解或者接近正解的代码,并不能测自己写的暴力代码。如果某道题自己只会写一档暴力,则必须要多测试。对于特殊性质的部分分,如果不好自动化批量对拍,则一定要多手造几组测一下。
平时做题写代码都是写正解,但是考场上写的大部分代码都是暴力、拍子之类的,并且代码量明显比平时要大,这一定程度上反应了训练和比赛时写的代码存在 gap。平时各种正解打得很爽,但比赛时未必就能把部分分写好,部分分也是要专门练习的。
一种尝试性的 OI 赛制代码架构,或许可以在写部分分时代码更干净一些:
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
const int N = 2e5 + 10;
/*
全局数据区,主要存储原始输入数据
对于每个 subtask,它可能还会用到其他的数据结构
比如哈希表,比如 vis 数组,比如记录暴搜过程中结果的 res 等
这些数据放在对应的 namespace 里面
不同的 subtask 需要相同的其他数据结构,除非是所有 namespace 都需要这个数据结构
否则最好也是各自在自己的 namespace 中开出来
*/
int n, x, y, m, a[N], b[N], c[N];
/*
每个 subtask 写一个 namespace,可以根据测试点编号给 namespace 命名
每个 subtask 的 solve 函数运行时,需要先做数据初始化,然后再执行逻辑
*/
namespace subtask_1_8 {
int vis[N], res;
void solve() {
/*
调试时,使用条件编译技术,将调试语句用 ifdef 和 endif 包裹起来
本地调试时,加上 -D LOCAL_DEBUG 宏,即可输出调试信息
提交后,由于评测环境中没有这个宏,所以不会输出调试信息
这样写相比于用 cerr 的好处是,在评测时不会带来输出的开销,不会因为调试语句超时
*/
#ifdef LOCAL_DEBUG
cout << "some current state" << "\n";
#endif
}
};
namespace subtask_9_13 {
map<int, int> cnt;
void solve() {
}
};
namespace subtask_14_20 {
int vis[N];
void solve() {
}
};
// 一些对输入数据分析的函数,用于判断到底分派给哪个 subtask 执行
bool all_equal() {
return true;
}
// 实现 subtask 的分发逻辑
void solve() {
/*
进行必要的读入,“必要”指的是足以判断出输入数据的特征
半数情况下,通过 n 的大小即可分发
另外半数情况,可能需要读入更多的数据,通过一些算法去判断是否有某种性质
这些算法可以实现在对应的 subtask namespace 里面,也可以写函数
但不建议直接在分派任务的 solve 里面写具体的检查逻辑
*/
cin >> n >> x >> y >> m;
if (n <= 100) {
subtask_1_8::solve();
} else {
for (int i = 1; i <= m; i++) {
cin >> a[i] >> b[i] >> c[i];
}
if (all_equal()) {
subtask_9_13::solve();
} else {
subtask_14_20::solve();
}
}
}
int main() {
/*
本地测试时定义宏 LOCAL_TEST,用于禁用文件读写
提交后由于编译时没有这个宏,所以文件读写会被打开
*/
#ifndef LOCAL_TEST
freopen("shuffle.in", "r", stdin);
freopen("shuffle.out", "w", stdout);
#endif
ios::sync_with_stdio(false);
cin.tie(0);
// 用于应对多测
int T = 1;
// cin >> T;
while (T--) {
solve();
}
return 0;
}
本地运行时,可以使用 g++ a.cpp -o a -Wall -static -std=c++14 -O2 -D LOCAL_TEST
来编译。
本地调试时,可以使用 g++ a.cpp -o a -Wall -static -std=c++14 -O2 -DLOCAL_TEST -DLOCAL_DEBUG
来编译。