如何正確地在 HydroOJ 出題(非官方文檔,超級詳細)


如何正確地在 HydroOJ 出題

前言

本文同步發布於:HydroOJ

本博客不是官方文檔,僅供參考。同時我不能保證當 HydroOJ 更新時本博客依然有效。

為什么要寫個這個呢?還不是因為 HydroOJ 更新了不少東西,但是官方的幫助文檔FAQ測試題域都已經過時了,完全沒有新的功能的介紹,只能自己摸索,於是就想寫這么一篇文章造福大家(?)

當然,這篇文章的內容都是我艱難地摸索出來的,如果我漏了什么好用的 feature,歡迎私信或在評論區告知!(HydroOJ 博客沒有評論區

文章較長,快速定位你需要的內容請善用 Ctrl+F。

創建題目

首先進入一個自己有創建題目權限的域(如果沒有就創建一個),然后進入題庫點擊“創建題目”,就進到了編輯界面啦!這個界面相信大家都能看懂,就不多加說明了,寫好題面點創建即可。特別地,難度缺省則為自動計算。

接下來會跳到“文件”界面,要求你上傳測試點。你可以不立即上傳測試點,當然這一步總是繞不過的。下面接着講測試點的配置,分別舉了各種題型當例子,希望可以講明白 qwq

傳統題

我們以 A+B Problem 為例,講解傳統題測試點的配置。

首先你需要寫一個數據生成器,並在本地生成出所有測試點,當然 A+B 這種萌萌題直接手打也行。特別提醒,如果用 time(0) 之類的作為隨機數種子,為了保證數據強度,請確保沒有兩組數據在同一秒生成。(好像跑題了

把測試點保存為 plus1.inplus1.out 等的形式,注意文件名中必須帶有數字,否則可能無法正確識別。

然后把這些測試點文件拖到題目的“文件”界面里的“測試數據”,就上傳上去了,當然強大的 HydroOJ(不是打廣告)還支持在線編輯文件。

然后呢?然后就沒了,快點擊“遞交”測一測你的 A+B 吧~

如果需要自定義單個測試點分數,或者時空限制,你需要創建一個 config.yaml,包含如下內容:

score: 20     # 單個測試點分數
time: 1s      # 時間限制
memory: 256m  # 內存限制

客觀題

我們以 1+1 Problem 為例,講解客觀題測試點的配置。

HydroOJ 支持單選題和填空題,大概是出初賽題用的吧。

在編輯題目界面,在題面里寫如下內容:

- desc: 請從下面所給的 A、B、C 三個選項中選擇最佳選項。
  choices:
  - A. 1 + 1 = 1
  - B. 1 + 1 = 2
  - C. 1 + 1 = 3
- desc: 請完成填空:1 + 1 = ?

就准備好了一半。這部分大家對照着我的題面看看就能知道是什么意思了。

由於這題的測試點配置比較獨特,我們不再需要 xxx1.inxxx1.out 這種東西,只需要一個測試點配置文件 config.yaml

對於客觀題來講,文件配置大致如下:

type: objective # 告訴評測機這題是一道客觀題
outputs: # 答案列表,格式是 [答案, 分值]
  - [B. 1 + 1 = 2, 50] # 選 B. 1 + 1 = 2,得 50 分
  - ['2', 50] # 填 2,得 50 分

然后題面中的 desc 之類的奇怪東西就被替換成我們想要的單選框個填空了。

文件讀寫

如果題目用在模擬賽里的話,可能希望模擬真實比賽環境,加上文件讀寫,這個 HydroOJ 也是支持的。

我們依然舉 A+B Problem 為例子,這時候我們希望選手們從 plus.in 而不是標准輸入讀入數據,並將答案寫到 plus.out 而不是標准輸出。

類似於上面“傳統題”部分講的,先把測試點上傳上去,然后由於特殊需求,我們也需要寫一個 config.yaml

這個文件里面只需要寫明希望操作的文件名就好了,其他缺省會默認成傳統題的一般配置:

filename: plus

子任務和子任務依賴

我們依然以 A+B Problem 為例(誰叫這個最簡單呢

害怕腳造數據,或者有時候遇到這種困難,就是不同的亂搞的最差情況不同,卡了一個就放了另一個?沒關系,我們有子任務!

一個亂搞過了最大的部分分,卻在較強的小數據掛掉了?HydroOJ 還支持子任務依賴,就是只有通過了某些前置子任務,這個子任務才會計分,否則計 \(0\) 分。

config.yaml 里面如下配置:

subtasks: # 表示本題采用子任務
  - score: 20 # 這個子任務分值
    id: 0 # 子任務編號
    # type: min # min/max/sum,表示子任務得分怎么由所包含測試點計算得到,缺省默認 min
    # time: 1s # 可以給每個子任務設置不同的時空限制
    # memory: 256m
    cases: # 子任務包含的測試點列表
      - input: plus1.in
        output: plus1.out
  - score: 40
    id: 1
    cases:
      - input: plus2.in
        output: plus2.out
      - input: plus3.in
        output: plus3.out
  - score: 40
    id: 2
    if: [0, 1] # 子任務依賴,這個子任務得分需要 id 為 0、1 的兩個子任務都對
    cases:
      - input: plus4.in
        output: plus4.out
      - input: plus5.in
        output: plus5.out

感謝 @

小技巧:如果把測試數據命名為 xxx1-1.in xxx1-2.in xxx2-1.in xxx2-2.in 這種格式,就會自動歸類 subtask

自定義校驗器(Special Judge)

依然是 A+B Problem,這題沒有 SPJ 的必要,只是作為示例解釋如何使用。

首先你需要寫一個 checker.cc(名字可以隨便起,注意不是 .cpp),例如:

#include "testlib.h"

int main(int argc, char* argv[]) {
    setName("compares two signed integers");
    registerTestlibCmd(argc, argv);
    int ja = ans.readInt();
    int pa = ouf.readInt();
    if (ja != pa)
        quitf(_wa, "expected %d, found %d", ja, pa);
    quitf(_ok, "answer is %d", ja);
}

然后在 config.yaml 里面注明使用 SPJ 評測:

checker_type: testlib # 根據官方文檔,支持 default(忽略行末空格和文末回車), ccr, cena, hustoj, lemon, qduoj, syzoj, testlib,可以選用自己熟悉的,但我只用過 testlib
checker: checker.cc

PDF 題面

如果題目用在模擬賽的話,可能也希望使用 PDF 題面,這也是支持的 Link

首先要在我的文件Link)上傳 PDF 文件(其他格式也成),注意是我的文件而不是題目文件

然后題面這么寫就行:

@[doc](https://hydro.ac/d/rui_er/file/44/statement-a-plus-b.pdf)

記得把 url 改成自己上傳的文件的。

如果有需要展示 PPT 的話,把上面那行的 doc 改成 slide 就行。

ACM 賽制

這里說的不是比賽的賽制,而是題目的賽制。

HydroOJ 的比賽選 ACM 賽制好像一切問題都解決了,不過為了 ACM 練習准備我們還是配置一下。我才不會說是我造完這個才發現有過了。

A+B Problem

這個的實現不難想,拿 config.yaml 把所有測試點塞到一個子任務里,這個子任務記 \(1\) 分即可。

subtasks:
  - score: 1
    id: 0
    cases:
      - input: plus1.in
        output: plus1.out
      - input: plus2.in
        output: plus2.out
      - input: plus3.in
        output: plus3.out
      - input: plus4.in
        output: plus4.out
      - input: plus5.in
        output: plus5.out

理論上如果是省選以下模擬賽出題人之類的,看到這里就夠了,下面是一些特殊題目的配置方法。

提交答案題

單文件提答

A+B Problem,這次我把輸入都給你了,求出來輸出之后告訴我。我不要程序,只要輸出。

由於是單文件提答,我們要求你只提交一個文件,在每一行給出每個問題的答案。

只造一組數據(可以考慮多測來放多組),然后顯然需要配置一下 config.yaml

type: submit_answer # 告訴評測機這是個提答題

這就完了?確實。

提交方法比較不友善,點進遞交發現還是要選代碼語言,咋辦?交輸出還是交代碼?

讓你交輸出就交輸出啊,隨便選個你覺得可愛的語言直接交就行,就這樣:

2919
3
18
12958
19992

多文件提答

A+B Problem,上傳數據的時候格式不太一樣,由於是提答題評測機不想要你的輸入文件,因此輸入文件內容改成希望從壓縮包中讀取的文件名稱如 plus1.out,輸出文件不變。

至於 config.yaml,你還需要告訴評測機是多文件提答,如下:

type: submit_answer
subType: multi

交互題

Grader 交互(函數式交互)

這里吐槽一句:測試題庫里面那個函數式交互根本不是比賽中的函數式交互好嗎。。

於是自己造輪子,搞一個真正的 Grader 交互的 A+B Problem

我們先准備好 plus.h

//By: Luogu@rui_er(122461)
int inc(int);
int dec(int);
int myPlus(int, int);

然后是我們的 Grader,這里叫 plus.cc

//By: Luogu@rui_er(122461)
#include "plus.h"
#include <bits/stdc++.h>
using namespace std;

int inc(int x) {return x + 1;}
int dec(int x) {return x - 1;}

int main() {
	int x, y;
	assert(scanf("%d%d", &x, &y) == 2);
	printf("%d\n", myPlus(x, y));
	return 0;
}

考慮一下這種交互怎么實現,選手提交的代碼是一些函數,主函數和判題的一些操作在 Grader 里面,那自然就要把這兩個文件編譯到一起(多文件編譯)。

於是就需要知道交上去的文件被存成了啥名字,我在討論:(已解決)【提問】HydroOJ 是否支持傳統 Grader 交互題中提問了,得到的回答是,C 語言在 foo.c,C++ 語言在 foo.cc

HydroOJ 還支持自定義編譯方法:寫一個 compile.sh

於是就可以實現這一功能了。

最終運行時運行的是 ./foo,所以多文件編譯出來的名字要是這個。

compile.sh

g++ foo.cc plus.cc -o foo -O2

config.yaml

type: default # 傳統題!不是 interactive 交互題!
user_extra_files: # 被放到工作目錄下的文件
  - compile.sh # 用來編譯的
  - plus.h # 頭文件
  - plus.cc # 交互庫

這是答案示例:

//By: Luogu@rui_er(122461)
#include "plus.h"
#include <bits/stdc++.h>

int myPlus(int a, int b) {
	return inc(a) + dec(b); // 直接 a + b 也行,這只是展示一下可以調用我們給的函數
}

I/O 交互

大概是 CF 等在線網站比較常用的交互方式。

A+B Problem,這時我們需要寫一個交互庫了。

交互庫是干啥的?I/O 交互中是用來處理詢問和發送數據的,交互庫的標准輸入是提交的代碼的標准輸出,交互庫的標准輸出是提交的代碼的標准輸入。

本題的交互庫就是這樣:

//By: Luogu@rui_er(122461)
#include "testlib.h"
#include <bits/stdc++.h>
#include <random>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;

template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}

int main(int argc, char* argv[]) {
	setName("Interactor A+B");
    registerInteraction(argc, argv);
    rnd.setSeed(time(0)+clock()); // 測試數據不可避免地可能會在同一秒生成,於是下面幾行亂搞一下盡量生成得不同,親測有效
    mt19937 myRnd(time(0)+clock()*20);
    uniform_int_distribution<int> dist;
    rnd.setSeed(time(0)+clock()+rnd.next(0, 10000)+dist(myRnd)+dist(myRnd));
    int a = rnd.next(0, 10000); // 生成數據
    int b = rnd.next(0, 10000);
    printf("%d %d\n", a, b); // 發送給提交的程序
    fflush(stdout); // 記得刷新緩沖區!記得刷新緩沖區!!記得刷新緩沖區!!!
    int c;
    scanf("%d", &c); // 讀進來提交的程序給出的答案
    if(a + b == c) quitf(_ok, "Accepted! (%d + %d = %d)", a, b, c); // 並判斷
    else quitf(_wa, "Wrong answer. (%d + %d = %d, but %d found)", a, b, a+b, c);
	return 0;
}

顯然也需要一個 config.yaml,如下:

type: interactive # 交互題
interactor: interactor.cc # 我們的交互庫
cases:
- input: /dev/null # 沒有輸入和答案,數據是交互庫動態生成的,所以留空
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null
- input: /dev/null
  output: /dev/null

通信題

這個我還沒搞好,搞好之后再補,可以先參考 @ 的博客 Link

遠端評測題(Remote Judge)

不知道為啥它掛了。

特殊題目

Quine

經典的非傳統題了,寫一個程序輸出自己源代碼,包含至少 \(10\) 個非空格的可見字符。

沒找到現成的題,自己造的。

小知識:HydroOJ 供 SPJ 獲取的存放源代碼的文件叫 user_code

准備一組空的 1.in1.out,只是占位用,顯然這題評測不需要測試點。

類似上面說的 SPJ,我們先配置 config.yaml

checker_type: testlib
checker: checker.cc

然后考慮 SPJ 咋寫。

我們已經知道咋獲取源代碼了,就好辦多了,直接讀文件比較即可,注意去掉行末空格、文末回車。

給個我的實現:

//By: Luogu@rui_er(122461)
#include "testlib.h" 
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false)
using namespace std;
typedef long long ll;

template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;}

int main(int argc, char* argv[]) {
	setName("quine checker");
    registerTestlibCmd(argc, argv);
    string pans = "", jans = "";
    ifstream cod("user_code"); // code stream
    int cnt = 0, lines = 0;
    while(!ouf.eof()) {
    	++lines;
    	pans = ouf.readLine();
    	getline(cod, jans);
    	int n = jans.length();
    	for(;jans[n-1]==' '||jans[n-1]=='\n'||jans[n-1]=='\r';--n);
    	jans = jans.substr(0, n);
    	if(pans != jans) quitf(_wa, "Wrong answer on line %d. (Expected '%s', but '%s' found)", lines, jans.c_str(), pans.c_str());
    	for(auto i : pans) if(i >= 33 && i <= 126) ++cnt;
	}
	if(cnt < 10) quitf(_wa, "Code is too short.");
	quitf(_ok, "Accepted! (%d characters)", cnt);
	return 0;
}

其他特殊題目

那就要看你具體想干啥了,仿照 Quine 自己寫一個 SPJ 試試吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM