蒟蒻 OIer の 考场操作与常见问题综合
前言
更新日志:
- 【本版,管理大大可以只看这些内容】2/11/2025:根据@Halberd_Cease,更新了
std::shuffle的用法。
建议参加 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。然后就挂了
我们不希望任何人重蹈此人的覆辙。
也许是一种在入场前迅速调整状态的方法
下述茶叶也可以用咖啡代替。不过本人喝不惯咖啡且茶叶比咖啡更加健康所以只能选择茶叶。
OIer 尤其是高中生的睡眠肯定是不足的。这里给出一个迅速调节的方案:
- 你需要准备一包袋泡茶(或同等量茶叶,但袋泡茶显然更加方便)、适量开水、适量冷水和能泡茶的容器(塑料的显然不行)。
- 用开水泡开茶叶,通常等几分钟就行。注意控制水量。
- 将冷水兑入上述过程得到的茶水中来迅速降低茶水温度。对笔者而言,总水量控制在
400\sim600 毫升为佳,你可以根据自己的情况调整。 - 在入场前
30 分钟左右喝下,确保能在入场前或入场几分钟内排出。 - 上述流程笔者参加中考时亲测有效,操作一次能顶半天。
如果你因为一些原因不能进行上述流程,那你可以买瓶冰红茶或者东方树叶之类含茶叶提取物的饮料,但效果肯定不如纯茶叶。
考试流程
试机
这通常在前一天晚上举行,你有最多半小时的时间。当然也不排除遇到去年 SWUT 那种 J 的试机时间都开始了结果还在调试导致 S 都还没捞着进去的情况。
- 试机的时候就找到插排在哪,防止踢线。如果能碰到的话提醒一下有踢你线风险的同学。
- 弄清楚虚拟机位置、开放盘位置。
- 充分地试用鼠标、键盘、显示器。不要不好意思找老师换,即使是哪个键按的时候明显卡顿而影响操作也可以找,你交了好几百不是去受苦遭罪的。
- 给机器测个速。你可以写一些常用模板,或者循环进行常用操作(取模等)。在本地机运行下述程序也可以直观地看出机器的速度(虽然这种方法测出的速度并不总是准确的,但用于横向比较时能够适用大多数情况,重要的是好写),数值大小参考 https://www.cnblogs.com/JuRuoOIer/p/speedtest.html,一般来说
4\times10^8\sim6\times10^8 的数字意味着比较可信,再大的话(比如 SDFZ 测出速度在10^9 以上)压测接近1 秒就要考虑卡卡常了。
#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;
}
入场
不要携带关机的手机进考场。
不要携带开机的手机上厕所。
可以带吃的。可以带饮料。但一般来说最多带点糖或者喜欢喝的糖分饮料就行了。
不可以带演算纸,考场会发。演算纸用完了就只管要,一张纸才几分钱他收你这么多钱不可能连张纸都买不起。
开考前
在开放盘会有一个压缩包,里面有试题和大样例。
正常来说是开考前
这里的一个小技巧是直接把大样例解压到你的选手文件夹里,然后子文件夹就不用你自己建了,而且里面还带着大样例。不过为了保险我更建议先解压到开放盘根目录再复制一份到选手文件夹,省的如果需要重新解压还得再输一遍密码。
说说文件夹和文件的命名问题。
选手文件夹的结构应该是(如果考场说的和这里不一样,以考场为准):
- 考号+姓名(不用打加号)
- 第一题英文名
- 第一题英文名.cpp
- 第二题英文名
- 第二题英文名.cpp
- 第三题英文名
- 第三题英文名.cpp
- 第四题英文名
- 第四题英文名.cpp
正确示范:
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 的第一页(应该?),通常来说是源代码不能超过
如果需要打表,可以考虑进行进制压缩:将原始数据压缩成 " 和反斜杠 \ 外所有 ASCII 可见字符及空格共有
交卷
老师会让你把多余的东西删掉。但如果时间确实紧张你就不删,只要你那个 .cpp 在就能正常交。
时间不紧张的前提下建议最后留 .ans 文件里了!);再然后查你的快读快输有没有和关同步流混用;最后查你交上去的版本到底是不是你想交的,每次都有人交错程序版本挂大量分。然后你可以好心地删掉答案外的多余文件。
考场通常会让你把开放盘根目录打开然后走人就行,但有的考场会要求你用极域交,这种情况后面说明了操作方法。
其他
- 上厕所要打报告。
- 周围有人发出声音等影响你做题的,情况严重找老师换备用机,轻的话就忍忍吧,毕竟换机器要时间的。
- 周围有人抄你代码的通常来说你不用管。
- 困的话去洗把脸。
- 如果已经想不到新的分了,可以进行对拍。方法在后面。
常用操作
显示文件全名
这里介绍一下 Windows 中显示拓展名的操作方式(这里以学校的 Win7 机器为例,Win10/11 类似):
打开“此电脑”。
在弹出的窗口中,
此时文件就显示全名了。
如果你对此不熟悉,你应该在试机的时候(可以带手机进去)就操作几遍。
DEV-C++ 基本操作
自动保存
直接将文件存到不还原的盘(黑板上一般有,没有问监考老师)。然后给编辑器设置自动保存。如果不希望保存到答案里(例如一种未必能写完的做法)应该新建文件,并在最后检查的时候确认是否是想要提交的代码。DEV-C++ 设置自动保存的方法如下:
打开 DEV-C++,然后顶栏找到“工具 - 编辑器选项”。
在弹出的窗口中找“自动保存”选项卡,并按图操作。尤其注意下面两个选项,选错了将不能正常保存。
这样你的程序就会每分钟保存一次了。
切换语言
第一步和“自动保存”类似,不过这时需要进入“环境选项”窗口。然后直接改就行了。
调整编译选项
第一步和“自动保存”类似,不过这时需要进入“编译选项”窗口。然后直接改就行了。
常见的编译选项:
-std=c++98/11/14:指定语言标准。CCF 现在用的是 C++14。-O1/O2/O3:指定优化。CCF 现在用的是 O2。-Wextra警告更多,-Wall警告全部。-Werror警告视为错误。-Wl,--stack=<size>手动开栈,这里<size>是栈大小,单位字节。
虚拟机基本操作
笔者的 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 中输出,所以屏幕上不会再显示输入输出了。如果仍然希望从屏幕进行输入,可以暂时将这两行注释掉,但交之前一定记得取消注释,否则 fc 命令)对比输出是否相等就行。当然,这种情况也需要注意,在最后记得把输入文件名后边那个编号去掉,否则同样
文件流
在代码中包含 <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 没啥区别。下面单位均为秒。
读写 long long,空格分隔:
| 读入方式 | 第一组 | 第二组 | 第三组 | 平均 |
|---|---|---|---|---|
cin/cout |
Read= |
Read= |
Read= |
Read= |
cin/cout 关同步流 |
Read= |
Read= |
Read= |
Read= |
scanf/printf |
Read= |
Read= |
Read= |
Read= |
我主页上的 fread/fwrite |
Read= |
Read= |
Read= |
Read= |
| wa90 的快读快写 | Read= |
Read= |
Read= |
Read= |
fstream |
Read= |
Read= |
Read= |
Read= |
读写长 char*:
| 读入方式 | 第一组 | 第二组 | 第三组 | 平均 |
|---|---|---|---|---|
cin/cout |
Read= |
Read= |
Read= |
Read= |
cin/cout 关同步流 |
Read= |
Read= |
Read= |
Read= |
scanf/printf |
Read= |
Read= |
Read= |
Read= |
我主页上的 fread/fwrite |
Read= |
Read= |
Read= |
Read= |
| wa90 的快读(不支持写) | Read= |
Read= |
Read= |
Read= |
fstream |
Read= |
Read= |
Read= |
Read= |
本地调题
RE
有时程序会弹一个已停止运行窗口,关掉这个窗口后,程序会以
如果是 STL 的问题,检查:
- 是否创建了输入文件?
- STL 容器是否越界?包括程序是否试图为空的容器弹出值?
否则,检查:
- 数组等是否出现了越界?
- 是否出现了无限递归,或者开的栈不够大?手动开栈的方法见“基本操作”中改编译选项的部分。
- 是否出现了整数除以/模以
0 的情况?
上面列出的不是所有的情况。你应该仔细检查任何可能访问程序外的空间的地方。
TLE
如果运行了一会并没有 RE 但也没有结果,或者只是 TLE 了,你应该试着检查:
- 测试行是否删掉了?输出会增加很大的时间开销。
- 是否出现了死循环或者无限递归(但空间开销小而没爆栈)?试着在可能出现问题的循环或函数进行输出调试。
- 是否算错了复杂度,或者常数太大?
- 数据分治(如果有)是否正确?
- 是否为循环的终止条件设置了时间开销大的函数?例如
for(int i=0;i<strlen(s);i++)的复杂度是|s|^2 的。 - 是否多测时进行了时间开销大的初始化?例如每次都进行
memset或者build,在仅保证\sum n 的大小而可能出现特别大的T 时会很慢。 - 读写速度是否不够快?
- 模数开成
const了吗?
MLE
如果你测出你的程序空间开销过大,那也许下面的建议会有用:
- 直接使用
std::map的[]运算符进行查找时,如果没找到会开一个导致额外的空间开销。如果你用到了可能查找不到的mp[]操作(其中mp是std::map),则换成mp.find()。 - 递归里有些东西可能可以开在递归外,开在递归内会重复占用空间。
不过也不要总担心空间。long double,所以其他的类型开
-
-
-
-
-
unsigned类型与signed类型所占空间相同。
本地 AC 但提交会炸
大部分可以通过进虚拟机编译一下来解决。同时编译选项 -Wall 也可以找到一部分问题。
- 万能头的斜杠方向问题。
- 正确示范:
#include<bits/stdc++.h>。 - 错误示范:
$include<bits\stdc++.h>。 - 另外,DEV-C++ 选手不建议用万能头,因为这会导致代码补全功能部分不可用(只要不是你自己定义的都不可用)。
- 正确示范:
- 数学库等部分库传入值的问题。
- 错误示范:
abs(__int128)。不能传这玩意。
- 错误示范:
- 程度较轻的越界。因为你本地机的 RAM 相对比较干净,一般越出去都是
0 。- 一种可以降低出错率的方法是看大样例数据是否满,没满的话自己造个满的跑。
- 有符号整数溢出其实是 UB。
- 注意
scanf/printf的占位符对应问题,尤其是喜欢#define int long long的选手。打开-Wall开关可以报出这个问题。 - 注意有返回值函数的返回值问题。打开
-Wall开关通常可以报出这个问题。 - 注意检查变量名冲突。不放心就开虚拟机编译,这个问题如果存在一定能报出来。
其他常见挂分问题
- 多测不清空,爆零两行泪。
- 注意输出格式。CSP-S2 2024 中,
- SD-S00374 在
detect一题中的数据间输出了两个换行,并因此挂了20 分。 - SD-S00496 在
duel一题中输出前换了一行,并因此挂了100 分。
- SD-S00374 在
如何对拍
你需要准备一个生成器 gen.cpp、一个暴力(或者其他你确定有正确性的东西)brute.cpp 和你需要拍的程序 sol.cpp。
如果每组数据要跑一秒以上,那你可以直接使 gen.cpp 以 time(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。
反之,你应该让对拍器从文件中读入种子,而种子由你的对拍器生成,这样每次的种子是不一样的。由于需要关闭文件,你只能使用文件流。
另外,如果拍不出问题,你应该检查你的暴力是否足够暴力,因为你以为正确的一些性质可能干脆是错的;或者试着手动构造更极端的情况进行压测。
下面提供一些常见输入的生成方法。
随机数
:::info[相关证据]
- 外国一选手使用
rand()填充了一个方阵,每个位置均为rand()%2生成。结果是:中间那一坨是啥??? - 此题答案为
12 ,使用rand()%210000 次来模拟扔硬币并重复该过程10^6 次得到的答案为p_{13}>p_{11}>p_{12} (不同种子运行多次都是相同的结果),但实际上p_{12} 是最大的。换成mt19937后没有这个问题。 :::
另外,通过扒头文件法我们知道 random_shuffle() 基于 rand(),所以同样需要 srand()。
介绍 rand() 的替代品 mt19937。
mt19937 在头文件 <random> 中(万能头中有),它基于梅森旋转算法。
定义
mt19937 rnd(time(0));
其中 rnd 可以换成你喜欢的(且不会冲突的)名字,time(0) 可以换成你喜欢的种子。
如果需要生成 mt19937_64 而非 mt19937。
调用
直接调用 rnd() 即可。
打乱数组
用法是 shuffle(vec.begin(),vec.end(),rnd);,rnd 即上述定义的 rnd(),但这里传入时不要带括号。
树
可以对每个点随机父亲,也可以随机 prufer 序列。二者的差别是前者的高度期望
:::info[OI Wiki 上关于 prufer 序列构造树的讲解]
Prüfer 序列的性质
- 在构造完 Prüfer 序列后原树中会剩下两个结点,其中一个一定是编号最大的点 𝑛。
- 每个结点在序列中出现的次数是其度数减 1。(没有出现的就是叶结点)
用 Prüfer 序列重建树
重建树的方法是类似的。根据 Prüfer 序列的性质,我们可以得到原树上每个点的度数。然后也可以得到编号最小的叶结点,而这个结点一定与 Prüfer 序列的第一个数所对应的点连接。然后我们同时将这两个结点的度数减一。
讲到这里也许你已经知道该怎么做了。每次我们选择一个度数为 1 的编号最小的结点,与当前枚举到的 Prüfer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 1 的点,其中一个是结点
𝑛 。把它们连上。使用堆维护这个过程,在节点度数下降的过程中如果发现度数减到 1 就把这个结点添加到堆中,这样做的复杂度是𝑂(𝑛log𝑛) 的。 :::
森林的话直接从树上断几条边就行。
当然,OI-Wiki 上提供了一种更好但更麻烦的生成树的方式。如确有需要可以记一下。
图
- 普通图:随机边即可。
- 连通图:先随一棵树然后连剩下的边。
- 无重边:把边
(u,v) 压缩为整数c=10^7u+v ,拿个map 维护一下就行。 - DAG:先随一棵树,定为外向树,然后其它边只生成终点深度不低于起点的边(每次随两个点判一下其实就行)。
- 二分图:随机左部点和右部点,然后随机横跨边即可。注意随机图是二分图的概率极低。