蒟蒻 OIer の 考场操作与常见问题综合

· · 算法·理论

前言

更新日志:

建议参加 CSP-J2/S2 2025 的选手,尤其是第一次参赛和第一次用 NOI Linux 的选手,读完这个 blog。

整理了一些在洛谷讨论区和近年迷惑行为大赏等地见到的及我自己干过的错误能想到的考试能用到的东西。欢迎补充,欢迎报锅。

如果你看过了https://www.luogu.com.cn/article/u99oll2a,这里解释了很多操作。

另外借楼预告今年的山东省迷惑行为大赏\~类似于往年,会查所有有汉字或者有关键词的代码哦。

特别提醒

最后检查的时候关程序不要着急,尤其看一眼关的时候有没有跳保存框,有的话你可能就是误触了错误示范(CSP-S2 2024,SD-S00793):

n#include<bits/stdc++.h>

是的,他不小心往前敲了个 n。然后就挂了 100

我们不希望任何人重蹈此人的覆辙。

也许是一种在入场前迅速调整状态的方法

下述茶叶也可以用咖啡代替。不过本人喝不惯咖啡且茶叶比咖啡更加健康所以只能选择茶叶。

OIer 尤其是高中生的睡眠肯定是不足的。这里给出一个迅速调节的方案:

如果你因为一些原因不能进行上述流程,那你可以买瓶冰红茶或者东方树叶之类含茶叶提取物的饮料,但效果肯定不如纯茶叶。

考试流程

试机

这通常在前一天晚上举行,你有最多半小时的时间。当然也不排除遇到去年 SWUT 那种 J 的试机时间都开始了结果还在调试导致 S 都还没捞着进去的情况。

#pragma GCC optimize(0)
#include<iostream> 
#include<cstdio> 
#include<cstdlib> 
#include<ctime> 
#include<fstream> 
#include<algorithm> 
#include<map> 
#include<queue> 
#include<deque> 
#include<set> 
#include<vector>
#define ll long long
#define lf double
#define ld long double
using namespace std;
ll tot,tot2,st;
int main(){
    for(int i=0;i<3;i++){
        tot=0;
        st=clock();
        for(int i=0;i<100000000;i++)tot++;
        tot=100000000/((clock()-st)*1.0/CLOCKS_PER_SEC);
        cout<<tot<<' ';
        tot2+=tot;
    }
    cout<<tot2/3;
    return 0;
} 

入场

不要携带关机的手机进考场。

不要携带开机的手机上厕所。

可以带吃的。可以带饮料。但一般来说最多带点糖或者喜欢喝的糖分饮料就行了。

不可以带演算纸,考场会发。演算纸用完了就只管要,一张纸才几分钱他收你这么多钱不可能连张纸都买不起。

开考前

在开放盘会有一个压缩包,里面有试题和大样例。

正常来说是开考前 10 分钟会发压缩包密码,5 分钟会发试题 pdf 密码。一般除个别管得严的考场(SD CSP-S 时好像没有这样的考场),进场其实就可以开始操作了。

这里的一个小技巧是直接把大样例解压到你的选手文件夹里,然后子文件夹就不用你自己建了,而且里面还带着大样例。不过为了保险我更建议先解压到开放盘根目录再复制一份到选手文件夹,省的如果需要重新解压还得再输一遍密码。

说说文件夹和文件的命名问题。

选手文件夹的结构应该是(如果考场说的和这里不一样,以考场为准):

正确示范:

F:\考前\SD-00001张三
├─arena
│      arena.cpp
│
├─color
│      color.cpp
│
├─detect
│      detect.cpp
│
└─duel
        duel.cpp

另外注意别把自己名字打错了。

错误示范(CSP-S2 2024,SD-S00513):内层文件夹名不应该带 .cpp

C:\USERS\DELL\DESKTOP\CSP-S2 2024 SD\ANSWERS\SD-S00513
├─arena.cpp
├─color.cpp
│      color.cpp
│
├─detect.cpp
│      detect.cpp
│
└─duel.cpp
        duel.cpp

错误示范(CSP-S2 2024,SD-S01025):文件名应该是 <题目英文名>.cpp

C:\USERS\DELL\DESKTOP\CSP-S2 2024 SD\ANSWERS\SD-S01025
├─arena
│      擂台游戏(arena).cpp
│
├─color
│      染色(color).cpp
│
├─detect
│      超速检测(detect).cpp
│
└─duel
        决斗(duel).cpp

错误示范(CSP-S2 2024,SD-S01150):文件名应该是 <题目英文名>.cpp

C:\USERS\DELL\DESKTOP\CSP-S2 2024 SD\ANSWERS\SD-S01150
├─arena
├─color
│      SD-S01150贾耀康.cpp
│
├─detect
│      SD-S01150贾耀康.cpp
│
└─duel
        SD-S01150贾耀康.cpp

文件的命名错误,一个是拼写问题,自己拿不准就复制。去年 SD 的情况是这样的(错误拼写后括号内数字为这么拼的人数):

duel    deul (1) dule (3)
detect  dectect (1) defect (1) delect (1) detcet (2) decete (1)
color   colour (1)
arena   arene(1) arenna (1) arent (1) area (1) arean (3) arema (1)

严重程度可见一斑。

另一个经典的问题是文件名后面 .cpp.cpp,因为第二个 .cpp Windows 好心地给你隐藏了。把它显示出来的操作方法见下文。

还有就是不太容易出现的爆代码长度问题。这个一般是会写在题面 PDF 的第一页(应该?),通常来说是源代码不能超过 100 KB。

如果需要打表,可以考虑进行进制压缩:将原始数据压缩成 64 进制(大小写字母、数字外加两个特殊符号即可实现)甚至 93 进制(除双引号 " 和反斜杠 \ 外所有 ASCII 可见字符及空格共有 93 个)字符串,可以有效减少长度。

交卷

老师会让你把多余的东西删掉。但如果时间确实紧张你就不删,只要你那个 .cpp 在就能正常交。

时间不紧张的前提下建议最后留 5 分钟检查文件问题。如果交卷前有时间检查,你应该先查刚才说的文件名、文件夹问题,再确定一次自己有没有开显示扩展名的选项;然后查每个文件内的文件读写是否开着,文件名是否对(注意别写到 .ans 文件里了!);再然后查你的快读快输有没有和关同步流混用;最后查你交上去的版本到底是不是你想交的,每次都有人交错程序版本挂大量分。然后你可以好心地删掉答案外的多余文件。

考场通常会让你把开放盘根目录打开然后走人就行,但有的考场会要求你用极域交,这种情况后面说明了操作方法。

其他

常用操作

显示文件全名

这里介绍一下 Windows 中显示拓展名的操作方式(这里以学校的 Win7 机器为例,Win10/11 类似):

打开“此电脑”。

在弹出的窗口中,

此时文件就显示全名了。

如果你对此不熟悉,你应该在试机的时候(可以带手机进去)就操作几遍。

DEV-C++ 基本操作

自动保存

直接将文件存到不还原的盘(黑板上一般有,没有问监考老师)。然后给编辑器设置自动保存。如果不希望保存到答案里(例如一种未必能写完的做法)应该新建文件,并在最后检查的时候确认是否是想要提交的代码。DEV-C++ 设置自动保存的方法如下:

打开 DEV-C++,然后顶栏找到“工具 - 编辑器选项”。

在弹出的窗口中找“自动保存”选项卡,并按图操作。尤其注意下面两个选项,选错了将不能正常保存。

这样你的程序就会每分钟保存一次了。

切换语言

第一步和“自动保存”类似,不过这时需要进入“环境选项”窗口。然后直接改就行了。

调整编译选项

第一步和“自动保存”类似,不过这时需要进入“编译选项”窗口。然后直接改就行了。

常见的编译选项:

虚拟机基本操作

笔者的 Arbiter 好像寄掉了,所以没写。

使用 Code::Blocks 编写、编译程序

打开虚拟机中的 Code::Blocks 软件,界面如下。

点中间的“Create a new project”,进入到下图所示的界面:

在左边找到“Files”,在中间找到“C/C++ Source”,然后单击右边“Go”,进入另一个向导窗口。点两次“Next”后出现下图界面;

这里设置保存位置的步骤比较抽象:

必须手动输入 .cpp,否则后面会没法接编译器。

然后写个 A+B 试试吧。Code::Blocks 里面 F9 是编译并运行(相当于 DEV-C++ 中 F11),Ctrl+F9 是编译(相当于 DEV-C++ 中 F9),Ctrl+F10 是运行(相当于 DEV-C++ 中 F10)。我们按下 F9。

他提示我未编译???

其实是,路径里不能带任何中文,否则都编译不了。于是安排了英文路径,这样就可以了:

使用共享文件夹与实体机互传文件

实体机的共享文件夹是虚拟机目录下的 share 文件夹,如图。

进入文件夹,我们往里面贴一个我们刚才演示命名用的张三文件夹:

虚拟机的共享文件夹为 计算机>mnt>hgfs>share。首先打开“计算机”:

然后找到对应的目录即可。图中我们新建一个文件夹,

此时回到实体机就会发现文件夹也跟着到了实体机。

极域基本操作

隐藏浮动工具栏

顶上那个极域的工具栏太傻逼了,我能关掉吗?

点开托盘极域图标(可能被折叠在小三角里了),然后单击“显示浮动工具栏”取消勾选(如果勾选的了话)。

提交文件或文件夹

如果考场让你提交文件,通常来说老师应该已经打开了收取文件的功能,此时屏幕上会有提交文件的窗口。如果没有:

打开这个窗口。然后把你的选手文件夹拖进去:

点提交,等进度条走完即可。

常见代码问题

读写问题

OI 赛事通常要求使用文件读写。首先介绍两种文件读写方式,然后下文对比不同方式的读写速度。

:::info[小贴士] 下面两种方法在使用时应该确保程序所在目录有输入文件,否则会在运行时报错。

但不需要保证有输出文件,没有会自动创建。

如果需要大量生成文件(比如造数据),必须用文件流。 :::

:::warning[注意] 输出到 .out 文件而不是 .ans 文件。 :::

文件重定向

在代码中包含 <cstdio> 头文件(或者万能头),并将下述代码加到你的 main 函数最开头:

freopen("test.in","r",stdin);
freopen("test.out","w",stdout);

test.in/out 换成你需要读写的文件。此时你的输入会变为从程序所在目录的 test.in 文件中读入(如果没有程序会爆炸),输出会变为向 test.out 中输出,所以屏幕上不会再显示输入输出了。如果仍然希望从屏幕进行输入,可以暂时将这两行注释掉,但交之前一定记得取消注释,否则 0 分。一种更方便的方案是按照我在“考试流程”部分说明的:将大样例贴一份到你的选手文件夹里。此时你可以直接在输入文件的后面加样例编号,然后打开输出文件(或者用 fc 命令)对比输出是否相等就行。当然,这种情况也需要注意,在最后记得把输入文件名后边那个编号去掉,否则同样 0 分。

文件流

在代码中包含 <fstream> 头文件(或者万能头)。

定义

像变量一样,在全局定义即可。

ifstream fin;
ofstream fout;

fin/fout 可以换成你喜欢的名字,后面步骤一律要跟着换,因为它类似于一个变量。

打开文件

格式为:

fin.open("test.in");
fout.open("test.out");

test.in/out 换成你需要读写的文件。为了和 freopen 一样快速地实现切换屏幕读写/文件读写,你可以在开头加:

#define cin fin
#define cout fout

这样后面就可以写 cin/cout,并随时取消注释来变为屏幕读写。

输入输出

cin/cout 完全一致。

关闭文件

fin.close();
fout.close();

在大量生成数据的时候,打开下一个文件前应该先关闭当前文件,这样程序才能正常运行。

速度对比

经测,开不开 O2 没啥区别。下面单位均为秒。

读写 10^7long long,空格分隔:

读入方式 第一组 第二组 第三组 平均
cin/cout Read=19.732,Write=1.561 Read=19.397,Write=1.595 Read=19.467,Write=1.673 Read=19.532,Write=1.610
cin/cout 关同步流 Read=2.955,Write=1.702 Read=2.555,Write=1.698 Read=2.628,Write=1.719 Read=2.713,Write=1.706
scanf/printf Read=11.578,Write=27.728 Read=11.147,Write=27.586 Read=10.924,Write=27.797 Read=11.216,Write=27.704
我主页上的 fread/fwrite Read=5.818,Write=6.588 Read=5.737,Write=6.693 Read=5.696,Write=6.652 Read=5.750,Write=6.644
wa90 的快读快写 Read=1.529,Write=1.391 Read=0.800,Write=1.390 Read=0.806,Write=1.415 Read=1.045,Write=1.399
fstream Read=2.056,Write=1.699 Read=2.004,Write=1.697 Read=2.020,Write=1.723 Read=2.027,Write=1.706

读写长 2\times10^8char*

读入方式 第一组 第二组 第三组 平均
cin/cout Read=20.332,Write=0.484 Read=22.505,Write=0.501 Read=19.574,Write=0.468 Read=20.804,Write=0.484
cin/cout 关同步流 Read=8.359,Write=0.485 Read=4.893,Write=0.483 Read=4.436,Write=0.514 Read=5.896,Write=0.494
scanf/printf Read=13.367,Write=6.047 Read=7.559,Write=6.324 Read=10.468,Write=6.191 Read=10.465,Write=6.187
我主页上的 fread/fwrite Read=7.079,Write=5.960 Read=6.140,Write=5.965 Read=6.637,Write=5.934 Read=6.619,Write=5.953
wa90 的快读(不支持写) Read=3.085 Read=2.167 Read=1.534 Read=2.262
fstream Read=1.249,Write=0.469 Read=0.702,Write=0.470 Read=0.702,Write=1.000 Read=0.884,Write=0.646

本地调题

RE

有时程序会弹一个已停止运行窗口,关掉这个窗口后,程序会以 255 或者一个 3 开头的巨大数(取决于你是否等到报错窗口的条走完)作为返回值,如果 STL 出问题还会蹦出一堆洋文。这种情况下就是你的程序 RE 了。

如果是 STL 的问题,检查:

否则,检查:

上面列出的不是所有的情况。你应该仔细检查任何可能访问程序外的空间的地方。

TLE

如果运行了一会并没有 RE 但也没有结果,或者只是 TLE 了,你应该试着检查:

MLE

如果你测出你的程序空间开销过大,那也许下面的建议会有用:

不过也不要总担心空间。128 MB 可以开 8\times10^6long double,所以其他的类型开 10^7 个以内根本不用算。各类型的空间如下:

本地 AC 但提交会炸

大部分可以通过进虚拟机编译一下来解决。同时编译选项 -Wall 也可以找到一部分问题。

其他常见挂分问题

如何对拍

你需要准备一个生成器 gen.cpp、一个暴力(或者其他你确定有正确性的东西)brute.cpp 和你需要拍的程序 sol.cpp

如果每组数据要跑一秒以上,那你可以直接使 gen.cpptime(0) 为种子生成数据。一种对拍器的实现:

ll i=0;
do{
  i++;
  cout<<"-----Test case "<<i<<"-----\n";
  system("gen.exe");
  system("brute.exe");
  system("sol.exe");
}
while(system("fc my.out ac.out"));

显然运行上述程序前应该编译你准备的三个 .cpp

反之,你应该让对拍器从文件中读入种子,而种子由你的对拍器生成,这样每次的种子是不一样的。由于需要关闭文件,你只能使用文件流。

另外,如果拍不出问题,你应该检查你的暴力是否足够暴力,因为你以为正确的一些性质可能干脆是错的;或者试着手动构造更极端的情况进行压测。

下面提供一些常见输入的生成方法。

随机数

\color{red}\text{注意:如果需要保证随机数的均匀程度,}\\\large\textbf{不要使用}\ \texttt{rand()}\ \textbf{函数!}\\\Large\textbf{不要使用}\ \texttt{rand()}\ \textbf{函数!}\\\LARGE\textbf{不要使用}\ \texttt{rand()}\ \textbf{函数!}

:::info[相关证据]

另外,通过扒头文件法我们知道 random_shuffle() 基于 rand(),所以同样需要 srand()

介绍 rand() 的替代品 mt19937

mt19937 在头文件 <random> 中(万能头中有),它基于梅森旋转算法。

定义

mt19937 rnd(time(0));

其中 rnd 可以换成你喜欢的(且不会冲突的)名字,time(0) 可以换成你喜欢的种子。

如果需要生成 64 位整数,则定义 mt19937_64 而非 mt19937

调用

直接调用 rnd() 即可。

打乱数组

用法是 shuffle(vec.begin(),vec.end(),rnd);rnd 即上述定义的 rnd(),但这里传入时不要带括号。

可以对每个点随机父亲,也可以随机 prufer 序列。二者的差别是前者的高度期望 \log n,后者的高度期望 \sqrt{n}

:::info[OI Wiki 上关于 prufer 序列构造树的讲解]

Prüfer 序列的性质

  1. 在构造完 Prüfer 序列后原树中会剩下两个结点,其中一个一定是编号最大的点 𝑛。
  2. 每个结点在序列中出现的次数是其度数减 1。(没有出现的就是叶结点)

用 Prüfer 序列重建树

重建树的方法是类似的。根据 Prüfer 序列的性质,我们可以得到原树上每个点的度数。然后也可以得到编号最小的叶结点,而这个结点一定与 Prüfer 序列的第一个数所对应的点连接。然后我们同时将这两个结点的度数减一。

讲到这里也许你已经知道该怎么做了。每次我们选择一个度数为 1 的编号最小的结点,与当前枚举到的 Prüfer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 1 的点,其中一个是结点 𝑛。把它们连上。使用堆维护这个过程,在节点度数下降的过程中如果发现度数减到 1 就把这个结点添加到堆中,这样做的复杂度是 𝑂(𝑛log⁡𝑛) 的。 :::

森林的话直接从树上断几条边就行。

当然,OI-Wiki 上提供了一种更好但更麻烦的生成树的方式。如确有需要可以记一下。