目录
目录README.md

template

这里是清华推研机试复刻组历史归档+后续协同造题/验题所使用的 tuack 模板。默认各位成员都会 git bash 操作,本地操作默认 Windows 系统。

以下介绍一下使用 git 以及 tuack 命题的常见操作。

1.本地配置多 git 账号

参考自沐爸:如何配置多个 Git 账号? - 知乎,如果大伙平时同时用 github gitee 以及公司/学校内部 git 的话,那么把多个 git 账号分开管理是非常必要的,尤其是在命题或其他私有项目当中,只对指定成员加了权限的情况下,就更需要配置好对应的 git 账户了。

1.清除全局配置

如果你已经全局配置过git账号,那么一定要先把全局的配置给清除掉。

git config --global --list  // 看一下是否配置过user.name 和 user.email
git config --global --unset user.name // 清除全局用户名
git config --global --unset user.email // 清除全局邮箱

2.删除生成过的全局秘钥

如果你已经全局配置过git账号,那么你一定生成过全局ssh密钥。 这个密钥通常在 ~/.ssh 目录下。

Windows 的一般在 C:\Users\username\.ssh 下面。

3.针对不同 git 生成对应的密钥

注意:必须在 C:\Users\username\.ssh 做对应操作

这里只讲 gitlink 怎么设置,别的都可以看上述知乎链接,或者有类似操作。

首先生成密钥,这里的用户名改成自己的

$ ssh-keygen -t rsa -C 'CYMario@gitlink.org.cn'

然后出现以下内容:

Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/14704/.ssh/id_rsa): 

我自己是将新的 rsa 分不同的 git 加后缀,所以这里输入了 id_rsa_gitlink

接下来还需要自己设置一个密码,空的也可以

$ ssh-keygen -t rsa -C 'CYMario@gitlink.org.cn'
Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/14704/.ssh/id_rsa): id_rsa_gitlink
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_gitlink
Your public key has been saved in id_rsa_gitlink.pub
The key fingerprint is:
SHA256:52wnFkAPMuqZU5yoB2zbwihiOBynEYBAohgkqKYJiiQ CYMario@gitlink.org.cn
The key's randomart image is:
+---[RSA 3072]----+
|&+    o o        |
|Bo.  + = o       |
|+o+.o + . .      |
|E*=* +   .       |
|#== B   S o      |
|O. o .   + .     |
|          * .    |
|         o o     |
|                 |
+----[SHA256]-----+

然后本地就会出现 id_rsa_gitlinkid_rsa_gitlink.pub 这俩东西。

4.在远端仓库添加公钥

gitlink 的在这里: https://www.gitlink.org.cn/settings/SSH

然后对着操作即可,新建一个 SSH 公钥,随便输入一个标题(一般是最好区分不同的电脑设备,因为不同电脑设备本地生成的密钥公钥不同)

公钥内容就把 id_rsa_gitlink.pub 的内容复制粘贴进去就行。

5.配置秘钥管理文件

还是在 C:\Users\username\.ssh 下面,新建一个 config 文件(无后缀),把多个 git 的配置都放在这里。

比如我的是这样,请注意 gitlink 的 Host 和 Hostname 前面都要带一个 code ,这一点和 github 还有 gitee 不一样。

Host gitee.com
Hostname gitee.com
User GoatGirl98
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_gitee

Host code.gitlink.org.cn
Hostname code.gitlink.org.cn
User CYMario
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_gitlilnk

Host github.com
Hostname github.com
User GoatGirl98
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_github

6.添加私钥到本地

这里直接 copy 上述文章的内容,在本地测应该是大差不差的。

// ssh-agent 是一个linux命令,主要是用来管理密钥的。

$  ssh-agent -s  // -s 生成Bourne shell 风格的命令输出
SSH_AUTH_SOCK=/tmp/ssh-XzAI1OaQvssj/agent.450; export SSH_AUTH_SOCK;
SSH_AGENT_PID=451; export SSH_AGENT_PID;
echo Agent pid 451;

$ ssh-add ~/.ssh/id_rsa_gitee
Could not open a connection to your authentication agent.

$ eval `ssh-agent -s`
Agent pid 460

$ ssh-add ~/.ssh/id_rsa_gitee // 将gitee私钥添加到本地
Enter passphrase for /c/Users/anzelin/.ssh/id_rsa_gitee:
Identity added: /c/Users/anzelin/.ssh/id_rsa_gitee (muba@qq.com)

$ ssh-add ~/.ssh/id_rsa_gitlab // 将gitlab私钥添加到本地
Enter passphrase for /c/Users/anzelin/.ssh/id_rsa_gitlab:
Identity added: /c/Users/anzelin/.ssh/id_rsa_gitlab (muba@163.com)

$ ssh-add -l  查看私钥列表
3072 SHA256:tT5SC8V7gAfqrdefrsC9PPckLyChSbIjS/jRO4 muba@qq.com (RSA)
3072 SHA256:JW/BArcdefg4+KxRrMkGsiAk8JoSFdefr8zMi0 muba@163.com (RSA)

7.测试仓库链接

在 git bash 里测试:

14704@LAPTOP-KAI4H021 MINGW64 ~/.ssh
$ ssh -T git@code.gitlink.org.cn
Hi there, CYMario! You've successfully authenticated with the key named 14704, but Gitea does not provide shell access.

差不多长这样就行了。

8.每个仓库单独设置配置

对于每个仓库,需要各自单独设置自己的用户名以及用户信息。以下的东西改成自己的名字和邮箱就行,gitlink 是只允许有一个邮箱的。

$ git config --local user.name "muba"

$ git config --local user.email "muba@163.com"

然后可以查看仓库配置信息

$ git config --local --list

在私有项目中,必须和项目添加权限用户的名称和邮箱都对应上,才能将本地 commit 成功 push 到仓库。

9.关闭本地仓库默认转 CRLF 文件的设置

我们的测试数据是要上 Linux 系统评测的(至少洛谷 LOJ TUOJ 这些都是),所以必须保证文件格式都是 Unix 的。本地生成了数据然后 push 的时候,我们不希望被项目自动转成 Windows 格式的。这里我们全局设置一下就可以了。

$ git config --global core.autocrlf false

此外,还可以加上

$ git config --global core.eol lf

这样统一换行符就都是 lf 了。更具体的可以见 https://www.cnblogs.com/song0916/p/15065932.html

剩下的什么 git add .git commit -m "blablabla"git push origin master 这些 git commit 的基础操作我就不说了。

此外,还需要在项目当中加上 .gitattributes ,保证我们协同工作的时候,必要的文本可以自动从 CRLFLF

*.cpp    eol=lf
*.java   eol=lf
*.py     eol=lf
*.yml    eol=lf
*.yaml   eol=lf
*.txt    eol=lf
*.in     eol=lf
*.ans    eol=lf
*.out    eol=lf
*.md     eol=lf
*.json   eol=lf
*.pyinc  eol=lf

这样就稳妥了。对于已经做过 commit 的工作,本地缓存过的文件,直接清除本地缓存,然后 .gitattributes 就生效了。

git rm -r --cached .
git add .
git commit -m 'update .gitattributes'
git push origin master

10.测试

成员们可以在 https://www.gitlink.org.cn/thupost-remake/sample (目前为私有项目)自己测试能否 git push 成功,以及 push 上去的数据是否能自动转换为 LF。

2.tuack 本地工作流常用的指令

更细致的见 https://gitee.com/mulab/oi_tools/wikis/ ,反正大部分的内容我们是不用的。

本地工作主要是用 cmd 输入各种命令。

具体的 tuack 题面怎么造,不太建议看模板,直接看成型项目的 statements 就行,然后照猫画虎做就 OK 了。

首先我们需要本地有 python pip tex 这些东西,tex 的话我本地安装的是 TeX Live 2021/W32TeX ,反正随便整一个就行,最好是完整版,一次安装就安装完整库的。

具体安装: pip install tuack

使用 python -m tuack.ren tuoi 渲染 pdf 题面

python -m tuack.test 可以测试 conf.yaml 里记载的代码,但是本地测试非常不准,而且测不了 SPJ 题目。图一乐就好。不过我们精选的实例都是可以保证在 windows 本地直接跑的。如果你只想测单独一个题目,或者只测一个代码,那么具体指令请看上面的 tuack 使用手册。

但是 conf.yaml 内部对于数据配置,数据点的描述很详细。命题人和验题人各部分代码在哪,对应多少分写的也很规范,值得我们借鉴并且使用。

剩下文件夹, down 是下发样例,pre 是样例,resources 是图库,solution 是题解,statement 是题面,tables 用不上(我们就用markdown原生表格就行)。

1.数据分数 conf.yaml 的配置

如果是捆绑测试的话,一般是这样:

核心就是要把 cases 这里按照这样的格式做好,然后下一行写这个子任务的分值。

users 这里照猫画虎就行了,表示大家的代码对应能拿多少分,在哪个文件夹。

args:
  n: 1000
compile:
  c: -O2 -std=c11
  cpp: -O2 -std=c++14
  java: ''
  pas: -O2
  py: ''
data: 
  - cases: ["101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119", "120", "121", "122", "123", "124", "125", "126", "127", "128", "129", "130"]
    score: 17
  - cases: ["201", "202", "203", "204", "205", "206", "207", "208", "209", "210"]
    score: 24
  - cases: ["301", "302", "303", "304", "305", "306", "307", "308", "309", "310", "311"]
    score: 24
  - cases: ["406", "407", "408", "409", "410", "411"]
    score: 35
folder: problem
memory limit: 512 MiB
name: drops
partial score: false
pre: []
samples: 
  - cases: [1]
time limit: 2.0
title:
  zh-cn: 水滴
type: program
users:
  CYMario:
    std:
      expected: == 100
      path: CYMario/std.cpp
    std2:
      expected: == 100
      path: CYMario/std-delink.cpp
    bf:
      expected: == 41
      path: CYMario/41pts.cpp
  jhdonghj:
    std:
      expected: == 100
      path: jhdonghj/drops.cpp
version: 2

如果是传统给分的话,就是下面这样:

args:
  n: 1000
compile:
  c: -O2 -std=c11
  cpp: -O2 -std=c++14
  java: ''
  pas: -O2
  py: ''
data: 
  - cases: ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
folder: problem
memory limit: 512 MiB
name: phigames
partial score: false
pre: []
samples: 
  - cases: [1, 2, 3]
time limit: 1.5
title:
  zh-cn: Phi 的游戏
type: program
users: 
  CYMario:
    std:
      expected: == 100
      path: CYMario/std.cpp
  jby:
    std:
      expected: == 100
      path: jby/phigame.cpp
version: 2

就没有那个 score 了。

userstitle zh-cnnamesample cases 都自己手动加就行,数据我们可以用以下内容做批量化生成:

#include <stdio.h>
/*
subtask
  - cases: ["101", "102", "103", "104", "105", "106", "107", "108", "109", "110"]
    score: 50
*/
/*
traditional
  - cases: ["101", "102", "103", "104", "105", "106", "107", "108", "109", "110"]
*/
inline void gen_interval_subtask(int dataidL, int dataidR, int width, int score)
{
    printf("  - cases: [");
    for (int i = dataidL; i <= dataidR; ++i)
    {
        printf("\"%0*d\"", width, i);
        if (i == dataidR) printf("]\n");
        else printf(", ");
    }
    printf("    score: %d\n", score);
}
inline void gen_interval_traditional(int dataidL, int dataidR, int width)
{
    printf("  - cases: [");
    for (int i = dataidL; i <= dataidR; ++i)
    {
        printf("\"%0*d\"", width, i);
        if (i == dataidR) printf("]\n");
        else printf(", ");
    }
}
int main()
{
    freopen("conf-part.yaml", "wb", stdout);
    gen_interval_subtask(101, 110, 3, 50);
    gen_interval_subtask(201, 215, 3, 30);
    gen_interval_subtask(301, 305, 3, 20);
}

捆绑测试用上面,传统测试用下面。

  • dataLdataR 表示数据名称范围
  • width 是位宽,可以相应添加前导零(然后在 conf 里输出成字符串形式,就可以带前导零了)
  • score 是每个子任务的成绩

输出出来的片段直接复制到 conf.yaml 对应的片段就行了。

2.如果想使用本地 tuack 测 SPJ 内容怎么弄?

参见 mc123456 的洛谷文章:如何优雅的出一套 oi 题

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);)。

总之就是和洛谷的 checker 没啥区别。

具体实例可以见 清华预推免机试 2021 校外(复刻) T1 和 T3。

此外 SPJ 可能存在爆栈问题,开足栈空间即可。

由于 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

3.洛谷工作流的数据分数配置

1.基础官方文档参考

我们主要需要参考洛谷的官方文档为:

清华推研机试题目只涉及传统题(无通信/交互题),评分只分为传统(所有测试点分值直接加和)与捆绑测试(子任务内必须通过所有点才获得分数,各子任务分数加和)。因此其他部分暂时无需查阅。

2.数据配置限制

具体规则在上面题目测试点配置文件写的比较清楚了。在我们这里只考虑传统题。

  • 输入输出需要正好对齐
  • SPJ 题必须有一个 checker.cpp ,需要遵循 testlib 写法(见SPJ 功能说明
  • (补充:tuack 本地的 SPJ 基本和这个差不多,怎么改动看上面就行了)
  • 分数点配置 config.yml ,之后我们所有题都要带上这个

非洛谷管理员在洛谷配置数据无法完全自由设置数据,单个评测链接会有如下限制:

  • 测试点个数不能超过 100
  • 所有测试点总评测时间上限不能超过 120 秒
  • 单组测试点时间上限不能超过 5 秒,内存上线不能超过 512 MB
  • 所有测试点 + 配置 + SPJ 等文件的压缩包后缀是 .zip,上传的 ZIP 文件不得超过 50MB

之后如果真的要做 OJ 迁移的话,我们可以进行自由设置,也不需要因为文件问题对数据拆分评测链接。不过目前我们先按照这个来做,需要拆分的题目也比较少,迁移时改动的工作量也不大。(主要还是要弄成 tuack 的形式,才方便迁移)

此外,在洛谷评测时必须先上传数据,否则评测结果就是未知错误(UKE)。

3.数据配置规范

这里我们不建议使用洛谷手动调整,因为一旦涉及数据改动就太麻烦了。之后针对所有题目,我们都需要写 config.yml 进行配置包括传统计分题目,一个是有些题目不是默认的 1s 512 MB,此外如果洛谷寄了,以后可能还会迁移到基于 UOJ 自己搭建的 OJ,有规范化的配置文件还是会方便很多)。

洛谷是对每一个输入文件,定义它的子任务、分值、时间限制(单位 ms)、空间限制(单位 KB)

102.in:
  timeLimit: 1000
  memoryLimit: 524288
  score: 50
  subtaskId: 1

这个表示的就是,102.in 这组评测数据,时限 1s,空间限制 512MB,所属子任务 1 的分值是 50。

在配置的时候,一般一个子任务的所有数据点,这几个参数都是一样的(事实上在 tuack 当中,所有子任务的时间空间限制也都必须一样)。

捆绑子任务在洛谷上的配置选项为:

  • 各个子任务内部使用“最小分值,最大时间”
  • 总分选“加和”
  • 可见性选“显示Subtask”。

如果是传统题的话,则子任务编号统一设成 1 即可,本地生成 config.yml 时,每个点均分满分(一般是 100 分,如果需要拆分数据则按照拆分的满分计算)。上传之后分数选择“加和”即可。

如果想把样例也放进去一块评测,则单独设置一个得分为 0 的子任务 0 即可。

我个人一般用以下代码自动化生成对应的 config.yml

#include <stdio.h>
const int T = 100, M = 1024;
/*
101.in:
  timeLimit: 2000
  memoryLimit: 524288
  score: 11
  subtaskId: 1
*/
inline void print_yml(int dataid, int width, int tlim, int memlim, int score, int subid)
{
    printf("%0*d.in:\n", width, dataid);
    printf("  timeLimit: %d\n", tlim * T);
    printf("  memoryLimit: %d\n", memlim * M);
    printf("  score: %d\n", score);
    printf("  subtaskId: %d\n\n", subid);
}
inline void gen_interval(int dataidL, int dataidR, int width, int tlim, int memlim, int score, int subid)
{
    for (int i = dataidL; i <= dataidR; ++i)
        print_yml(i, width, tlim, memlim, score, subid);
}

int main()
{
    freopen("config.yml", "wb", stdout);
    gen_interval(101, 131, 3, 6, 512, 13, 1);
    gen_interval(201, 204, 3, 6, 512, 15, 2);
    gen_interval(301, 310, 3, 6, 512, 19, 3);
    gen_interval(401, 410, 3, 6, 512, 24, 4);
    gen_interval(501, 510, 3, 6, 512, 29, 5);
    gen_interval(601, 620, 3, 50, 512, 20, 6);
}

其中 gen_interval 就是从哪个数据点到哪个数据点这一个连贯的区间采用相同的子任务配置,接口如下:

  • dataidLdataidR ,数据点编号的左右闭区间。因此需要我们生成的数据点编号分布是连续的
  • width 数据点编号位宽,不足时会补齐前置 0 (例如位宽为 2,1 号点输出就会是 01.in)
  • tlim 时间限制。一般上面的 T 常量会设置为 1000,然后 tlim 传入秒数即可。但是这题通常子任务是 0.6 秒,所以 T 改成了 100。
  • memlim 空间限制,传入单位是 MB。所以输出时需要乘以常量 M 1024。
  • score 分数,数据点本身或者其所在的子任务分数是多少,就用多少即可。
  • subid 子任务编号,直接传入即可,无需前导0。

如果是传统给分,则按照下面这样做:

const int T = 1000, M = 1024;
int main()
{
    freopen("config.yml", "wb", stdout);
    gen_interval(1, 20, 2, 3, 512, 5, 1);
}

给 20 个测试点,每个点 5 分,时限 3 秒,512 MB,就这样改参数就 OK 了。

4.评测数据的格式

0.文件格式

由于我们不是 oi 赛制,因此评测文件名称不需要带英文单词,只需要是数字就行。

数字编号的分布最好是子任务内连贯的,此外为了方便在 git 上显示,数据点编号的命名最好带前导 0。

输入文件的后缀是 .in ,输出文件格式的后缀是 .ans,历史工作我们也会逐一修改。

1.文件内容

首先需要注意的是,文件必须是 Unix 格式的,因为我们的文件是需要放在基于 Linux 系统的评测机上评测(洛谷 LOJ 清华的dsa oj/土豆oj/西红柿oj 都是)。

这个比较好办,我们在输出期望文件时,文件读写这样:

freopen("1.ans", "wb", stdout);

就可以了。

除了一些特殊的字符串大模拟题之外,行末尾不要有多余的空格。比如说 $n$ 个用空格分隔的整数,那第 $n$ 个整数后面就不能有空格,而是要换行。历史评测数据的问题我们会慢慢修复。

输入输出数据最后末尾要多一个空的换行。比如如果输出答案只有一个 3 ,那么文件这样:

3

这样能够保证在输出文件只有一行时,该文件依旧是 Unix 格式的(否则 Windows 上生成出来就是 Windows CRLF 格式了)。

不管文件是否只有一行,我们都要保证多一个空行。

2.历史文件转换

如果你的历史文件需要把换成 LF 格式,以及文件末尾加上多余换行,以及去掉通常行末的空格字符,可以将测试点放在一个文件夹下,然后先后运行一下两个代码:

import os

# 获取当前文件夹路径
current_directory = os.getcwd()

# 列出所有文件和文件夹
files_and_folders = os.listdir(current_directory)

# 过滤出文件
files = [f for f in files_and_folders if os.path.isfile(os.path.join(current_directory, f))]

# 将文件名写入 result.txt
with open('result.txt', 'w') as result_file:
    for file in files:
        result_file.write(file + '\n')

print(f'已将当前文件夹下的所有文件输出到 result.txt')

这样可以获取所有文件名

然后跑以下 C++ 代码,即可批量化处理。

fwrite 不重要,重要的是文件读写是 wb。如果你的文件行末有多余空格,那么用 getline 和字符串处理就行了。这个之后再加。

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <string>
#include <iostream>
#include <sstream>
using namespace std;
const int SIZE = 100000000;
char buf[SIZE];
char f[100];
char cmd[100];
bool is_out(char* s) { return strstr(s, "out") != nullptr; }
inline void strip(char *s)
{
    FILE *fin = fopen(s, "r");
    if (fin == NULL) return;
    size_t cnt = fread(buf, sizeof(char), SIZE, fin);
    fclose(fin);
    int len = strlen(s);
    // trans .out to .ans
    if (is_out(s))
    {
        sprintf(cmd, "del %s", s);
        FILE* fcmd = popen(cmd, "rb");
        fclose(fcmd);
        s[len - 3] = 'a', s[len - 2] = 'n', s[len - 1] = 's';
    }
    // final \n
    // if (buf[cnt - 1] != '\n') buf[cnt++] = '\n', buf[cnt] = '0';
    string BUF = string(buf);
    stringstream ss(BUF);
    string ans = "", li = "";
    while (getline(ss, li))
    {
        while (isspace(li.back())) li.pop_back();
        ans += li, ans.push_back('\n');
    }
    while (isspace(ans.back())) ans.pop_back();
    ans.push_back('\n');
    FILE *fout = fopen(s, "wb");
    fputs(ans.c_str(), fout);
    // fwrite(buf, sizeof(char), cnt, fout);
    fclose(fout);
    memset(buf, 0, cnt);
}
int main()
{
    freopen("result.txt", "r", stdin);
    while (scanf("%s", f) != EOF) strip(f), fprintf(stderr, "strip %s success.\n", f);
    return 0;
}

在 OJ 上测输入数据是否带 \r 也简单。直接提交以下代码即可:

#include <iostream>
#include <string>
#include <cassert>
using namespace std;
string s;
int main() 
{ 
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    while(getline(cin, s)) assert(s.back() != '\r'); 
}

如果得到的评测结果是 Wrong Answer,则说明所有输入文件的换行符都不存在 \r,是格式正确的。反之如果获得 Runtime Error,则说明输入文件存在 \r,需要进行批量化处理。

3.tuack/洛谷工作流的其他内容

以后再加,这个感觉照猫画虎就能搞的差不多了。或者现在随手加一些。

关于

清华推研机试复刻组工作归档/协同造题所使用的 tuack 项目模板

411.0 KB
邀请码
    Gitlink(确实开源)
  • 加入我们
  • 官网邮箱:gitlink@ccf.org.cn
  • QQ群
  • QQ群
  • 公众号
  • 公众号

©Copyright 2023 CCF 开源发展委员会
Powered by Trustie& IntelliDE 京ICP备13000930号