CSP-S模拟赛总结

记录一下 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 来编译。

赞赏