如何优雅的出一套 oi 题
SoundOfDestiny · · 个人记录
本文旨在搭建一个优雅的出题环境,并非如 洛谷日报 #192 的出题指南。
前因
最近想和同学出一套 NOIp 赛题,已经写好一部分代码和数据了,但是不知道用什么封装,然后想把数据上传到洛谷跑一跑也不行(数据量太大了),最后就打算自己部署一个简易的 oj(用的 Hydro),但是又没找着能输出 pdf 的 oj,所以又在本地部署了一套 tuack。
本文用到的电脑系统为 Windows 11,如遇下载缓慢请自行寻找镜像源或是进行科学上网,文章提及的版本均为截至文章完成时的最新版(或最新稳定版)。
IPv6 环境
这里以光猫、路由器、设备的三级网络结构为例,并不选择将光猫改为桥接模式(这样可以直接在光猫上接多路由器,可能更符合更多家庭的需求)。
首先访问 ip 查询网站,检查自己是否已经开启 IPv6,然后按打开 PowerShell 输入
ipconfig
并按回车。
如未开启 IPv6,请从“光猫开启 IPv6”开始操作;如已开启 IPv6,但是与 ipconfig 命令返回的地址不同,请从“路由器开启 IPv6”开始操作。
光猫开启 IPv6
在百度中搜索自己光猫对应的管理员密码(可以通过“地区+运营商”来搜索,如“北京电信光猫管理员密码”),这里以北京电信光猫为例,用户名为 telecomadmin 密码为 nE7jA%5m。
在浏览器中输入光猫地址(一般在光猫的背面有写,通常为 192.168.1.1),进入光猫后台,输入管理员账号密码登录光猫后台。依次选择“网络”→“网络设置”,连接名称选择带有 INTERNET 字样的选项,并将“IP 模式”更改为“IPv4&IPv6”,点击“保存”以保存更改。再依次选择“安全”→“防火墙”,如果“启用IPV6 SESSION”处于勾选状态,请取消勾选并点击“保存”以保存更改。
路由器开启 IPv6
在浏览器中输入路由器地址(一般在路由器的背面有写),进入路由器后台,输入管理员密码登录,这里以小米路由器为例,IP 地址为 192.168.31.1。依次选择“常用设置”→“上网设置”→“工作模式切换”,并将路由器切换为有线中继模式,等待一段时间后就完成了路由器的设置。
检查 IPv6 环境
打开 PowerShell,并输入 ipconfig,如果出现下列信息(主要看 IPv6 地址开头是否为 fe,如果设置正确,则不应为 fe),则说明 IPv6 环境配置完成,否则可能是设备或者运营商不支持 IPv6 造成的。
无线局域网适配器 WLAN:
连接特定的 DNS 后缀 . . . . . . . :
IPv6 地址 . . . . . . . . . . . . : 24xx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
临时 IPv6 地址. . . . . . . . . . : 24xx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
IPv4 地址 . . . . . . . . . . . . : 192.168.xxx.xxx
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : fe80::1%13
192.168.1.1
oj 搭建
我这次选择的是采用 VMware + Ubuntu Server + Hydrooj + IPv6 的搭建方案。
安装 VMware
首先来到 VMware 官网,向下划动找到 Workstation 17 Pro for Windows,点击立即下载,获取安装程序。
由于 VMware 不知名的 bug,VMware 17.0.2 无法为虚拟机分配可访问的公网 IPv6 地址,请不要将 VMware 升级至 17.0.2 版本,请用 17.0.0 版本。
双击打开安装程序,根据自己需求更改安装地址,然后无脑点下一步,直至下面有“许可证”选项,点击后输入许可证(下面是适用于 VMware 17 的许可证),然后提示要重启电脑,重启电脑后进行下一步。
4A4RR-813DK-M81A9-4U35H-06KND
NZ4RR-FTK5H-H81C1-Q30QH-1V2LA
JU090-6039P-08409-8J0QH-2YR7F
4Y09U-AJK97-089Z0-A3054-83KLA
4C21U-2KK9Q-M8130-4V2QH-CF810
MC60H-DWHD5-H80U9-6V85M-8280D
设置 VMware
到现在为止 VMware 已经安装完成,但我们要对它进行一些设置。
首先打开 VMware 后选择“编辑”→“虚拟网络首选项”→“更改设置”,提示需要管理员权限,然后选择“VMnet0”→“已桥接至”,打开下拉菜单后选择正在使用的网卡。然后再选择“VMnet8”→“NAT 设置”→“启用 IPv6”→“确定”,最后点击“应用”。
然后再选择“编辑”→“首选项”,更改“虚拟机的默认位置”到合适的文件夹,然后就完成了 VMware 的设置。
安装 Ubuntu Server
首先来到 Ubuntu 官网,点击 Download Ubuntu Server 22.04.3 LTS,等待下载完成,就获得了 Ubuntu Server 的系统镜像。
然后在 VMware 中选择“创建新的虚拟机”,选择“典型”,点击“浏览”,找到刚刚下载的 Ubuntu Server 的 iso 镜像文件,输入虚拟机名称,最大磁盘大小更改为 50 GB(可按需更改)。然后选择自定义硬件,内存分配 2048 MB 或以上(建议 4096 MB 或以上),处理器数量选择 1 或以上,每个处理器的内核数量选择 1 或以上(建议 2 或以上),网络适配器选择桥接模式并选择复制物理网络连接状态,这样我们便完成了虚拟机的创建。
创建完虚拟机后启动虚拟机,此时选择 Try or Install Ubuntu Server,等待一会后进入安装界面,在此界面可以通过上下方向键和回车键进行操作。接下来所有配置都选择默认配置(如果想自己设置的可以调节),然后输入账号密码,建议将名字、服务器名称和用户名同一设为一串纯英文小写字母(防止记乱或是出问题),这里用户名以 yyxc 为例,密码以 yyxcnasd 为例。接下来均选择默认配置,然后经过一小段时间的等待后重启就完成了 Ubuntu Server 的安装。
部署 Hydro
建议去看官方文档,里面有详细的安装指南和后期的维护指南,还有很多实用的教程,这里直接采用最简单的安装方案。
虚拟机重启后等待不再有文字弹出时,输入用户名 yyxc,按下回车再输入密码 yyxcnasd,再按下回车便进入了 Ubuntu Server 的命令行界面。此时我们在命令行中输入
ip a
按下回车后应该可以看到一串 192.168.xxx.xxx,这里以 192.168.1.28 为例,这就是这台虚拟机的局域网 ip 地址。此时我们再打开 Windows 自带的 PowerShell,输入
ssh [email protected]
其中格式为
ssh <username>@<ip>
按下回车后输入 yes,再按下回车输入密码 yyxcnasd,便使用 ssh 连接到了自己的 Ubuntu Server,现在我们就可以直接在 PowerShell 中粘贴命令了。
现在在 PowerShell 中输入
sudo su
按下回车后输入密码 yyxcnasd,便获得了 root 权限,此时再粘贴如下脚本
LANG=zh . <(curl https://hydro.ac/setup.sh)
等待一段时间后便完成了 Hydro 的安装,这是在 Windows 中的浏览器中输入 192.168.1.28 访问 Ubuntu Server,并注册一个账号,这里用户名密码仍然以 yyxc 和 yyxcnasd 为例。
注册完毕后输入
hydrooj cli user setSuperAdmin 2
pm2 restart hydrooj
等待一段时间后,便完成了整个 Hydro 的部署。
设置 Hydro
再次访问 192.168.1.28,选择“控制面板”→“系统设置”,调整以下设置:
- 调整
server.language为简体中文 - 调整
preference.codeLang为C++17(O2) - 调整
limit.problem_files_max为500 - 调整
testcases_max为500 - 调整
total_time_limit为3000 - 调整
memoryMax为2048m - 调整
parallelism为1 - 调整
singleTaskParallelism为1
其中
parallelism项和singleTaskParallelism如果不调整为1,可能会在大数据的情况下出现个别测试点超时。
这样便完成了 Hydro 的设置。注意,此时任何邮箱都可以注册,所以不要向外网公布你的地址(或者可以自己折腾 smtp 并设置防火墙和攻击保护),强烈建议在信任的老师同学间进行使用。
让其他人访问 oj
分两种方式,一种是 IPv6 直接访问,另一种是内网穿透,如果 IPv6 环境已经配置完成,建议选择 IPv6 直接访问(这里不使用 IPv4 是因为很多人都申请不到公网 IPv4)。
IPv6 直接访问
- 优点:不限带宽,不限流量,无需设置
- 缺点:任意一端不支持 IPv6 就会不能访问
打开 PowerShell,输入
ssh [email protected]
再输入密码 yyxcnasd,进行 ssh 连接,再输入
ip a
找到一串很长的 IPv6 地址,通常为 24xx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx,这就是 oj 的 IPv6 地址,我们在浏览器中粘贴这段地址并在前后加上中括号(长这样:[24xx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]),便可直接访问 oj。
此时也可以去购买一个域名(首年 10 元),并在 DNS 解析中添加 AAAA 记录,使用域名访问 oj。
使用内网穿透
- 优点:不限使用环境
- 缺点:有可能大幅限制上传下载速度
这里不多作介绍,可以使用 OpenFrp,这里也有详细的官方教程;或者使用 LoCyanFrp,这里也有对应的官方教程。
部署 tuack
其实可以直接用 Anaconda 集成环境,但是我还是喜欢用 VSCode,所以这里介绍如何在 VSCode 中安装并使用 tuack。
安装 Visual Studio Code
前往 Visual Studio Code 官网,选择 Download for Windows 下载安装包。
下载完成后运行安装包,安装时勾选“将'通过 Code 打开'添加到 Windows 资源管理器文件上下文菜单”和“将'通过 Code 打开'添加到 Windows 资源管理器目录上下文菜单”。
安装完成后打开 VSCode,选择左侧的插件栏,建议安装 Chinese、C++、Python、PowerShell 插件,其他可按需安装。下图中 CodeGeeX 插件为免费 AI 代码补全插件。
安装 C++ 编译器
前往 MinGW-w64 下载页面,选择 x86_64-win32-seh 进行下载(如遇无法使用的情况请查询电脑参数并选择合适的版本)。
下载完成后解压压缩包,并将解压出的文件夹放置到任意目录(建议在 C:\),然后找到文件夹中的 bin 文件夹,右键选择“复制文件地址”。然后按 win + i 打开设置,找到“系统信息”→“高级系统设置”→“环境变量”,并双击编辑“系统变量”中的 Path,选择“新建”,粘贴刚刚复制的文件夹地址并删掉前后引号,一路选择“确定”,即完成 C++ 编译器的安装。
安装完成后打开 PowerShell,输入
g++ --version
如果返回了编译器的版本信息则说明安装成功,如果未返回请自行解决。
安装 Python
前往 Python 官网,下载最新版(或是稳定版)Python,这里我选择的是目前最新的稳定版本 3.11.5 版本。
安装时请一定要勾选 Add python.exe to PATH,其他配置可以采用默认,然后无脑安装即可。
安装完成后打开 PowerShell,输入
python --version
如果返回了 Python 3.11.5 则说明安装成功,如果未返回大概率是未将 Python 添加至 Path,可参考 C++ 编译器安装方式。
安装 pandoc 及 xelatex
如果无需输出 pdf 格式题面,可跳过该步。
pandoc
直接前往 pandoc 仓库地址,下载 msi 文件进行安装。
xelatex
这里选择安装 MiKTeX 以实现对应功能。
前往 MiKTeX 官网下载 MiKTeX 安装包,选择为所有用户安装,其余选择默认配置,由于 MiKTeX 较大,安装可能较费时,请耐心等待。
安装 tuack
直接在 PowerShell 中输入
pip install tuack
即可完成安装。
优雅的出题
出题说简单又简单,说麻烦又麻烦,这里将过程简化为以下几步,可按需浏览。
准备工作
一个好的想法
或许一个好的想法是源于新学的知识点,或许源于一道好题,或许只是一次灵光乍现,但是不论怎样,优雅出题的第一步就是拥有一个好的想法。拥有了一个好的想法的同时也应当拥有对应的解法。
环境配置
在电脑的任意地方新建一个文件夹(下文中的所有文件夹和文件名称都不建议带有空格,可以用 - 或者 _ 替代),这里以 My-OI-Problem 为例。
打开 VSCode,并在 VSCode 中打开刚刚新建的文件夹,并在该文件夹中新建一个子文件夹用于存放第一套 oi 题,这里以 My-OI-1 为例。
在 VSCode 下方的命令行中输入
cd My-OI-1
python -m tuack.gen contest
python -m tuack.gen day day0
这样会将 My-OI-1 文件夹设置为一个比赛文件夹,并在该文件夹下新建一个名为 day0 的比赛日文件夹。再在命令行中输入
cd day0
python -m tuack.gen problem adder
这样会在 day0 比赛日文件夹下新建一道名为 adder 的题目文件夹,找到该文件夹下的 conf.yaml 文件,并编写题目配置。然后就可以愉快的在 VSCode 中出题了。
正解编写
我认为正解应当比题面先写出来(多人合作出题可以并行执行),所以这里先介绍正解的编写。
先在 adder 文件夹下新建一个 std 文件夹,用来存放正解程序,再在 std 中新建一个 std.cpp 用来编写正解,然后就可以愉快的编写了。
编写完成后可以选择右上角的运行来测试程序的正确性(编译器一定要选择 g++.exe)。
题面及题解编写
可以参考 OI Wiki,题面应在题目文件夹下的 statement 中,题解应在题目文件夹下的 solution 中。
数据生成器编写
先在 adder 文件夹下新建一个 gen 文件夹,并在该文件夹中编写数据生成程序。我习惯分别编写 data_in.cpp、data_hack.cpp(有时会与 data_in.cpp 合并为一个文件)、data_ans.cpp、data_re_in.cpp(强制在线时对输入数据加密会用到)、data.bat 几个文件,强烈建议使用 Testlib 编写,使用方法可以参考 洛谷日报 #271 和 OI Wiki,这里是几段参考代码。
// data_in.cpp
#include <bits/stdc++.h>
#include "testlib.h"
#define endl '\n'
using namespace std;
typedef long long ll;
const string NAME = "adder";
ll TMIN;
ll TMAX;
const ll TLEN = 2;
const bool SUBTSK = true;
ll SUBTSKID;
const ll SUBTSKLEN = 1;
string get_str(ll x, ll len)
{
string ret = "";
for (; len; len--, x /= 10)
ret += char(x % 10 + '0');
return string(ret.rbegin(), ret.rend());
}
pair<string, string> get_FILE(ll T)
{
string FILE_NAME = ".\\..\\data\\" + NAME + "_";
if (SUBTSK)
FILE_NAME += "sub" + get_str(SUBTSKID, SUBTSKLEN) + "_";
FILE_NAME += get_str(T, TLEN);
cout << FILE_NAME << endl;
return make_pair(FILE_NAME + ".in", FILE_NAME + ".ans");
}
void argtoinit(int argc, char **argv)
{
if (argc != 4)
{
cout << "Wrong Format!" << endl;
exit(-1);
}
SUBTSKID = atoi(argv[1]);
TMIN = atoi(argv[2]);
TMAX = atoi(argv[3]);
}
void init()
{
// Clear and init all the things here
}
ifstream fin;
ofstream fout;
int main(int argc, char **argv)
{
registerGen(argc, argv, 1);
argtoinit(argc, argv);
for (ll T = TMIN; T <= TMAX; T++)
{
auto IOFILE = get_FILE(T);
fout.open(IOFILE.first);
init();
// Writing .in generator here
fout.close();
}
return 0;
}
// data_hack.cpp should be similar as this program
// data_ans.cpp
#include <bits/stdc++.h>
#include "testlib.h"
#define endl '\n'
using namespace std;
typedef long long ll;
const string NAME = "adder";
ll TMIN;
ll TMAX;
const ll TLEN = 2;
const bool SUBTSK = true;
ll SUBTSKID;
const ll SUBTSKLEN = 1;
string get_str(ll x, ll len)
{
string ret = "";
for (; len; len--, x /= 10)
ret += char(x % 10 + '0');
return string(ret.rbegin(), ret.rend());
}
pair<string, string> get_FILE(ll T)
{
string FILE_NAME = ".\\..\\data\\" + NAME + "_";
if (SUBTSK)
FILE_NAME += "sub" + get_str(SUBTSKID, SUBTSKLEN) + "_";
FILE_NAME += get_str(T, TLEN);
cout << FILE_NAME << endl;
return make_pair(FILE_NAME + ".in", FILE_NAME + ".ans");
}
void argtoinit(int argc, char **argv)
{
if (argc != 4)
{
cout << "Wrong Format!" << endl;
exit(-1);
}
SUBTSKID = atoi(argv[1]);
TMIN = atoi(argv[2]);
TMAX = atoi(argv[3]);
}
void init()
{
// Clear and init all the things here
}
ifstream fin;
ofstream fout;
int main(int argc, char **argv)
{
registerGen(argc, argv, 1);
argtoinit(argc, argv);
for (ll T = TMIN; T <= TMAX; T++)
{
auto IOFILE = get_FILE(T);
fin.open(IOFILE.first);
fout.open(IOFILE.second);
init();
// Writing .ans generator here
// This program may similar as std.cpp
fin.close();
fout.close();
}
return 0;
}
% data.bat %
g++ -o data.exe -std=c++14 -Wl,--stack=1000000000 data_in.cpp
% Give data.exe arguments like 'data.exe <SUBTSKID> <TMIN> <TMAX>' %
data.exe 1 1 10
data.exe 2 1 10
g++ -o data.exe -std=c++14 -Wl,--stack=1000000000 data_hack.cpp
% Give data.exe arguments like 'data.exe <SUBTSKID> <TMIN> <TMAX>' %
data.exe 1 11 11
data.exe 2 11 13
g++ -o data.exe -std=c++14 -Wl,--stack=1000000000 data_ans.cpp
% Give data.exe arguments like 'data.exe <SUBTSKID> <TMIN> <TMAX>' %
data.exe 1 1 11
data.exe 2 1 13
全部编写完成后只要双击 data.bat 就会自动生成数据,如果启用子任务,文件名将会类似于 name_sub1_01.in,不启用则类似于 name_01.in。如果需要样例,则应当把对应的输入输出文件放到 down 文件夹下(预测试几乎用不到,不过需要用的可以把对应的输入输出文件放在 pre 文件夹下)。
运行完之后在 VSCode 底部的命令行中 cd 到对应题目文件夹,并输入
python -m tuack.gen auto
该命令可以自动检测新生成的数据、样例、预测试点和正解,并添加到题目配置文件中(虽然没有对子任务的识别,需要手动添加)。
Special Judge 编写
在 adder 文件夹下有一个 data 文件夹,在 data 文件夹中新建一个 chk 文件夹,并在 chk 文件夹中新建一个 chk.cpp,然后就可以愉快的编写 SPJ 了。
SPJ 的编写还是建议使用 Testlib,不过由于 tuack 不支持原版 Testlib,所以需要下载 Testlib for Lemons 来代替原版的 Testlib(请直接用 registerLemonChecker(argc, argv); 代替原本 chk.cpp 中的 registerTestlibCmd(argc,argv);)。
题面生成
费了这么大一趟搭建了这么个环境,肯定要生成 pdf 题面试试,在 VSCode 下面的命令行中 cd 到比赛文件夹,并输入
python -m tuack.ren noi
来生成 noi 风格的 pdf 题面,会自动保存在 statements 文件夹下。
上传题目至 oj(在线测评)
访问刚部署的 Hydro,选择“题库”→“创建题目”,使用 MarkDown 编写题面。完成后将题目的所有数据(即 data 文件夹下的所有数据和 data\chk 文件夹下的 chk.cpp 和 testlib.h)上传,然后在评测设置里选择比较器类型为 other,Interface 选择 lemon,比较器选择 chk.cpp,评测额外文件选择 testlib.h,即可正常使用 SPJ(如果没有 SPJ,则不需要调整设置)。再选择子任务栏目,自动检测并自动配置子任务,再根据需要配置每一个子任务的信息。
完成后选择递交,就可以上传程序了。
本地测评
可以直接使用 python -m tuack.test baoli.cpp(其中 baoli.cpp 应为选手程序文件名),或者使用 Lemon 进行本地评测,可以参考 OI Wiki。
由于 tuack 的一个 bug,
chk.cpp可能会出现爆栈情况,需要进入Python安装目录\Python311\Lib\site-packages\tuack中,将里面的base.py的第 596 行引号内的内容(不含引号)修改为g++ %s -o %s -O2 -std=c++14 -Wall -Wl,--stack=1000000000。
结语
自己折腾这么一大圈虽然很麻烦,但是真的很有成就感,而且看着 VSCode 里面一大堆的文件夹和文件,还有一点点生成出来的数据,就显得很有范(很优雅)。
希望大家也能享受折腾的乐趣,享受出题的乐趣。