From 8851519b67d1aac6569a384b2ccc1d5978da0fc3 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 9 Feb 2026 10:25:07 +0000 Subject: [PATCH] update spj docs --- .../luogu/problem/interactive-problems.md | 223 +++++++++----- docs/manual/luogu/problem/special-judge.md | 278 +++++++++++++----- docusaurus.config.ts | 1 - 3 files changed, 356 insertions(+), 146 deletions(-) diff --git a/docs/manual/luogu/problem/interactive-problems.md b/docs/manual/luogu/problem/interactive-problems.md index 041b2ba..6637bb0 100644 --- a/docs/manual/luogu/problem/interactive-problems.md +++ b/docs/manual/luogu/problem/interactive-problems.md @@ -1,114 +1,193 @@ # 交互题功能说明 -交互题即用户提交的程序,通过出题人提供的交互库,与判题程序(SPJ)进行交互并获得输入、解答问题。 +交互题即用户提交的程序,通过出题人提供的交互库,与判题程序进行交互并获得输入、解答问题。 -洛谷上的交互题评测,由以下三个部分组成:判题程序(SPJ)、交互库、用户程序。其中出题人需要提供 SPJ 以及交互库。交互题需要打上 **交互题** 标签。如果使用 SPJ 功能,还需要 **Special Judge** 标签。 +洛谷上的交互题评测,由以下三个部分组成:判题程序(评测机或者 SPJ)、交互库、用户程序。其中出题人需要提供交互库,并且根据试题需求可能需要 SPJ。交互题需要打上 **交互题** 标签。如果使用 SPJ 功能,还需要 **Special Judge** 标签。 -在数据压缩包中,除了测试点输入输出文件,还需包含 `checker.cpp` SPJ 程序文件、以及 `interactive_lib.cpp` 交互库文件。与 SPJ 相同,均使用 C++14 标准进行编译,不支持其他语言。 +在数据压缩包中,除了测试点输入输出文件,还需包含 `interactive_lib.cpp` 交互库文件。交互库与 SPJ 相同,均使用 C++14 标准进行编译,不支持其他语言。 -- 交互题的 SPJ 写法 +## 函数式交互题 -交互题可以不使用 SPJ 功能。不使用的情况下,交互库将直接读取测试点输入,并由默认比较器进行输出比较。和普通题目相比,除了链接交互库到用户程序外没有其他区别。 +函数式交互题是一种题目类型,其中参赛者的程序需要与判题系统进行交互,但这种交互的方式通常是通过函数调用的形式来实现,而不是通过标准输入输出流进行交互。也就是说,选手的程序与系统的交互是通过一组预定义的函数接口来进行的,而不是通过输入输出文本。 -交互题的 SPJ 与普通的 Special Judge 类似,使用 testlib。具体请参考 [Testlib 文档](https://github.com/MikeMirzayanov/testlib) 以及 [Special Judge 使用说明](./special-judge.md)。 +### 无需使用 SPJ 的情况 -需要注意的是,该 SPJ 同时承担判题和与交互库交换数据两个功能。交换数据通过标准输入输出进行,既 SPJ 的 `stdout` 可在交互库的 `stdin` 中读出,交互库的 `stdout` 可在 SPJ 的 `stdin` 中读出。 - -这里给出一个简单的例子: +以 [P13691 [CEOI 2025] highest](https://www.luogu.com.cn/problem/P13691) 为例,本题需要用户实现以下函数: ```cpp -#include "testlib.h" -#include +vector solve(vector &v, vector &w, vector> &queries); +``` -using namespace std; +那么本题的交互库所需要承担的行为是: +- 将输入数据读入进交互库; +- 调用 `vector solve(vector &v, vector &w, vector> &queries);` ,此时选手程序运行并且返回结果; +- 输出结果,与答案文件进行比较; -int main(int argc, char* argv[]) -{ - setName("Interactor A+B"); - registerInteraction(argc, argv); +换而言之,这类试题中,交互库的本质是帮助选手写完输入输出,选手只需编写核心程序。因此,它的交互库可以写作: - // 从测试数据中读取输入 - int a = inf.readInt(); - int b = inf.readInt(); +```cpp +#include +#include +using namespace std; +vector solve(vector &v, vector &w, vector> &queries); +int main() { + ios::sync_with_stdio(false); + cin.tie(nullptr); + int n; + cin >> n; + vector v(n); + for (int i = 0; i < n; i++) { + cin >> v[i]; + } + vector w(n); + for (int i = 0; i < n; i++) { + cin >> w[i]; + } + int m; + cin >> m; + vector> queries(m); + for (int i = 0; i < m; i++) { + cin >> queries[i].first >> queries[i].second; + } + vector results = solve(v, w, queries); + for (int i = 0; i < m; i++) { + cout << results[i] << "\n"; + } + fflush(stdout); + return 0; +} +``` - // 发送给交互库 - cout << a << " " << b << endl; +在文件目录下仅需 interactive_lib.cpp 即可,无需添加 checker.cpp,只需勾选交互题标签。 - // 读取交互库的输出。直接使用 cin 或者 scanf 也可以。 - int pa = ouf.readInt(); - int ja = a + b; +### 需要使用 SPJ 的情况 - if (ja != pa) quitf(_wa, "expected %d, found %d", ja, pa); - quitf(_ok, "answer is %d", ja); -} -``` +事实上,大部分函数式交互题目并不像上例那么简单。以 [P13612 [IOI 2018] combo 组合动作](https://www.luogu.com.cn/problem/P13612) 为例,本题需要选手实现函数 `string guess_sequence(int N)`,允许选手调用函数 `int press(string p)`,且根据函数调用次数反馈评分。这个时候需要用到 Special Judge 功能。在文件目录下需要 interactive_lib.cpp 和 checker.cpp,并且同时勾选交互题和 Special Judge 标签。 -- 交互库的写法 +在此类情况下,测试数据在 checker、interactive_lib 的流转如下: +- 从 checker 的 inf 流读取试题原本的测试数据,再使用 stdout 输出信息。 +- 此时,interactive_lib 可以从 stdin 输入 checker 输出的信息,进行处理,然后从 interactive_lib 的 stdout 输出信息。 +- 接着,checker 从 stdin(或者 ouf 流)获得 interactive_lib 输出的信息,进行处理。 +- 最后,checker 根据函数调用次数等信息,为选手的程序评分。 -交互库 `interactive_lib.cpp` 是被链接到用户程序的一个模块,包含或者不包含 `main` 函数均可。理论上如果你希望用户直接与 SPJ 通过标准输入输出交互,直接什么都不提供也可以,但这种情况也必须放一个空的文件。 +以上述试题为例,在测试数据内存放的是字符串 $S$,因此需要 checker 从 inf 流内输入 $S$。由于题目要求选手只应知道字符串的长度 $N$,因此需要将字符串 $S$ 的长度输出给交互库。 -建议交互库的所有需要用户调用的导出函数、以及需要用户定义的函数,都使用 `extern "C"` 关键字定义,以方便不同语言编写的用户程序调用这些函数。 +```cpp +// 这里是在 checker 内的行为 +S = inf.readLine(); // 从输入文件读入 S +N = S.length(); +std::cout << N << std::endl; // 将 S 输出给 interactive_lib +``` -下面给出如上同一题目的交互库例子: +在 interactive_lib 中,需要实现选手可以调用的函数 `int press(string p)`。通常而言的实现方式是,交互库通过输出,与 checker 进行交互,checker 可以从 stdin(或者 ouf 流)中读取交互库的需求,进行判断处理,然后重新输出给交互库。 ```cpp -#include +// 这里是在 interactive_lib 内的行为 +#include +#include + +std::string guess_sequence(int N); // 选手程序需要完成的部分 -extern "C" { - extern int plus(int a, int b); +int press(std::string p) { + printf("? %s\n", p.c_str()); // 输出 ? p,可以让交互库读取到请求。 + fflush(stdout); + int t; scanf("%d", &t); // 读入从 checker 提供的“当按出按键序列 p 后你赚到的金币数量。” + return t; // 返回给选手程序 } int main() { - int a,b; - std::cin >> a >> b; - std::cout << plus(a, b) << std::endl; - return 0; + int N; scanf("%d", &N); // 读入从 checker 获取的 N,然后将其传给选手的程序 + printf("! %s\n",guess_sequence(N).c_str()); + fflush(stdout); } ``` -显然该交互库需要用户定义 `plus` 函数来实现加法功能。正确的解答可以如此编写: - ```cpp -extern "C" int plus(int a, int b) { - return a + b; -} +// 这里是在 checker 的部分行为 +std::string cmd_str = ouf.readToken("[?!]", "cmd"); +// 读取 interactive_lib 发来的操作类型 +char cmd = cmd_str[0]; +if (cmd == '?') { + // 处理查询 + query_count++; + if (query_count > MAX_QUERIES) { + quitf(_wa, "too many moves: made %d queries, limit is %d", query_count, MAX_QUERIES); + } + std::string p = ouf.readToken(); + // 读取 interactive_lib 发来的函数调用申请 + if (p.length() > (size_t)4 * N) { + quitf(_wa, "invalid press: p is too long. length=%zu, max_len=%d", p.length(), 4 * N); + } + for (size_t i = 0; i < p.length(); ++i) { + if (VALID_CHARS.find(p[i]) == std::string::npos) { + quitf(_wa, "invalid press: p contains invalid character '%c' at index %zu", p[i], i); + } + } + int result = calculate_press_result(p); + std::cout << result << std::endl; // 将答案使用 stdout 发送给交互库 +} ``` -如果希望用户实现的函数调用交互库的函数,可以在 `extern "C"` 中实现。例如,如果在交互库中有一个 `inc()` 函数,用于给这个数字增加 1,那么交互库可以这么写: +## IO 交互题 -```cpp -#include +在 Codeforces、ICPC 上更常见的是 IO 交互题。参赛者的程序需要与判题系统进行交互,但交互是通过输入输出流(stdin 和 stdout)进行的。选手的程序与判题系统之间的交互是基于标准输入和输出流的读取和写入,而不是通过函数调用。 -extern "C" { - extern int plus(int a, int b); - int inc(int a){ - return a+1; - } -} +IO 交互题的配置相对简单,在文件目录下需要 checker.cpp 用作交互,以及空的 interactive_lib.cpp 用于占位。 -int main() { - int a,b; - std::cin >> a >> b; - std::cout << plus(a, b) << std::endl; - return 0; -} -``` +以 [P14843 [ICPC 2022 Yokohama R] Interactive Number Guessing](https://www.luogu.com.cn/problem/P14843) 为例,这是一道 IO 交互题,用户输出指令(query 或者 answer,在样例输出栏),交互库读取指令后输出给用户信息(在样例输入栏的正整数)。在此类交互题中,checker 可以通过 inf 读入试题测试数据,并且从 stdin 或者 ouf 读取选手程序的输出(即 stdout)。checker 可以从 stdout 输出信息到选手程序的 stdin,允许选手程序读入。 -正确的解答可以这么写: +本题的 checker 如下: ```cpp -extern "C" int inc(int a); // 需要声明这个函数 -extern "C" int plus(int a, int b) { - int k; - k = inc(a); // 调用函数 - return k + b - 1; // 其实没啥意义,仅供演示 -} -``` +#include "testlib.h" +#include +using namespace std; +using ll = long long; -- 特别需要注意的点 +static int digitsum(ll x) { + int s = 0; + while (x > 0) { s += int(x % 10); x /= 10; } + return s; +} -缓冲区问题。C/C++语言的输入输出函数均自带有一定的缓冲区,因此你输出的内容不一定能立刻被交互库/SPJ所读取。如果不在合适的时间清空缓冲区,很可能会造成两个程序互相等待对方输出的情况而 TLE。 +int main(int argc, char **argv) { + registerInteraction(argc, argv); + ll x = inf.readLong(); + int query_count = 0; + + while (true) { + if (ouf.seekEof()) quitf(_wa, "invalid type in query_count=%d", query_count); + string type = ouf.readToken(); + + if (type == "query") { + if (query_count >= 75) quitf(_wa, "query limit exceeded"); + if (ouf.seekEof()) quitf(_wa, "invalid query in query_count=%d", query_count); + ll a = ouf.readLong(); // 从 ouf/stdin 读取选手程序输出 + if (a < 0 || a >= 1000000000000000000LL) + quitf(_wa, "invalid range in query_count=%d: %lld", query_count, a); + cout << digitsum(a + x) << '\n'; // 从 stdout 将信息发送给选手程序,选手程序可以在 stdin 读取 + cout.flush(); + query_count++; + } else if (type == "answer") { + if (ouf.seekEof()) quitf(_wa, "invalid guess in query_count=%d", query_count); + ll guess = ouf.readLong(); + if (guess < 0 || guess >= 1000000000000000000LL) + quitf(_wa, "invalid guess range in query_count=%d: %lld", query_count, guess); + if (guess == x) { + if (!ouf.seekEof()) quitf(_wa, "Trailing output: '%s'", ouf.readToken().c_str()); + quitf(_ok, "accepted"); + } else { + quitf(_wa, "wrong guess: %lld actual: %lld", guess, x); + } + } else { + quitf(_wa, "invalid type: %s", type.c_str()); + } + } +} +``` -每次输出完一定内容、希望对面的程序开始进行处理时,都必须手动清空缓冲区。在 C 语言中,可使用 `fflush(stdout)` 函数。在C++语言中,可使用 `std::cout << std::flush`;在使用 `std::endl` 输出一个换行时,C++语言也会自动清空缓冲区。 +## 重要注意事项 -交互SPJ最好不要占用太多的 CPU 资源,因为它将与用户的程序在同一个核心上执行。最好在评测程序等待来自SPJ的输入,或者评测程序已经结束运行之后,再进行复杂的计算。 +1. 缓冲区问题。C/C++ 语言的输入输出函数均自带有一定的缓冲区,因此你输出的内容不一定能立刻被交互库/SPJ 所读取。如果不在合适的时间清空缓冲区,很可能会造成两个程序互相等待对方输出的情况而 TLE。 +2. 每次输出完一定内容、希望对面的程序开始进行处理时,都必须手动清空缓冲区。在 C 语言中,可使用 `fflush(stdout)` 函数。在 C++ 语言中,可使用 `std::cout << std::flush` ;在使用 `std::endl` 输出一个换行时,C++ 语言也会自动清空缓冲区。 +3. 交互库(以及 SPJ)最好不要占用太多的 CPU 资源,因为它将与用户的程序在同一个核心上执行。最好在评测程序等待来自 SPJ 的输入,或者评测程序已经结束运行之后,再进行复杂的计算。 \ No newline at end of file diff --git a/docs/manual/luogu/problem/special-judge.md b/docs/manual/luogu/problem/special-judge.md index cbaa279..d5904bf 100644 --- a/docs/manual/luogu/problem/special-judge.md +++ b/docs/manual/luogu/problem/special-judge.md @@ -1,149 +1,281 @@ # Special Judge 功能说明 -当一个题目可以接受多种正确答案,即有多组解的时候,题目就必须被 Special Judge。 +当一道题目的答案不唯一,即存在多组符合要求的解时,必须使用 Special Judge(以下简称 SPJ)进行评测。 -Special Judge 程序使用输入数据和一些其他信息来判答你程序的输出,并将判答结果返回. +SPJ 程序会根据输入数据以及其他相关信息来判定选手程序的输出是否正确,并返回评测结果。 -洛谷的 SPJ 采用了跟 Codeforces 一样的 SPJ 标准,即 Testlib 库。 +洛谷采用的 SPJ 标准与 Codeforces 一致,均基于 Testlib 库。 下载地址: [https://github.com/MikeMirzayanov/testlib/releases/download/0.9.41/testlib-0.9.41.zip](https://github.com/MikeMirzayanov/testlib/releases/download/0.9.41/testlib-0.9.41.zip) -Testlib 库 0.9.41 版 引入了一些重大变更,具体见:[https://github.com/MikeMirzayanov/testlib/releases/tag/0.9.41](https://github.com/MikeMirzayanov/testlib/releases/tag/0.9.41) - -Checker 的编译参数为:`g++ -fno-asm -std=c++14 -O2`,即已经开启 C++14 以及 O2 优化。 +在洛谷上,SPJ 的编译参数为:`g++ -fno-asm -std=c++14 -O2`,即已经开启 C++14 以及 O2 优化。 ## 使用方法 -只能使用 C++。不过写 spj 就跟写别的题目一样,只是输入输出有所不同。首先新建文件 checker.cpp。然后将这个压缩包的里的所有内容解压到你的 checker.cpp 相同的文件夹。 +### 基础教程 + +SPJ 仅支持使用 C++ 编写。其编写方式与普通题目类似,主要区别在于输入输出的处理。首先创建文件 `checker.cpp`,并将上述压缩包中的所有内容解压至与该文件相同的目录下。 -这里给出一个例子,当标准输出和选手输出的差小于 0.01,那么可以 AC,否则 WA。 +以下为一个简单示例:若选手答案和标准答案输出的差值小于 $0.01$,则判定为通过(AC),否则判定为错误(WA)。 ```cpp #include "testlib.h" int main(int argc, char* argv[]) { - registerTestlibCmd(argc, argv); - double pans = ouf.readDouble(); - double jans = ans.readDouble(); + setName("compares two doubles"); + registerTestlibCmd(argc, argv); // 初始化 checker,必须在最前面调用一次。 + double pans = ouf.readDouble(); // 从选手输出读取一个 double 类型变量 + double jans = ans.readDouble(); // 从标准答案读取一个 double 类型变量 - if (fabs(pans - jans)<0.01) + if (fabs(pans - jans) < 0.01) // 如果差值小于 0.01,返回答案正确 quitf(_ok, "The answer is correct."); - else + else // 否则返回答案错误 quitf(_wa, "The answer is wrong: expected = %f, found = %f", jans, pans); } ``` -在程序中,有 3 个重要的结构体:inf 指数据输入文件(本例没有),ouf 指选手输出文件,ans 指标准答案。 +在 Testlib checker 中,不使用 cin/cout 来读写判题数据,而是使用三条输入流(由评测系统提供): -然后,可以从这 3 表结构体读入数据,不需要用到标准输入输出。如果读到的数据和下面的期望不一致,则 spj 返回 fail 结果。 +- `inf`:测试输入文件(题目输入数据),在上例中未使用; +- `ans`:标准答案文件(参考输出/最优值/一组可行解等,不一定唯一); +- `ouf`:选手输出文件(你要判定的对象); -这边继续给出一个多行(不定行数)的 spj 判断: +Checker 的职责通常是验证 ouf 是否满足题意(例如:选手的输出是否是一组合法的可行解)。 -```cpp -#include "testlib.h" +在 testlib.h 中,有四种常见的答案判定: +- `_ok`:通过本题; +- `_wa`:答案错误; +- `_pe`:格式错误(洛谷目前不支持此类评测结果); +- `_fail`:checker 自身异常,**不要用它表示选手答案错误**; -int main(int argc, char* argv[]) { - registerTestlibCmd(argc, argv); +对于 checker,有以下的常用指令,可以用于读入测试数据: - while(!ans.eof()){ - double pans = ouf.readDouble(); - double jans = ans.readDouble(); - ans.readEoln(); +- `void registerTestlibCmd(argc, argv)`:初始化 checker,必须在最前面调用一次。 +- `char readChar()`:读入一个 char,指针后移一位。 +- `char readChar(char c)`:和上面一样,但是只能读到一个字母 c。 +- `char readSpace()`:同 readChar(' ')。 +- `string readToken()`:读入一个字符串,但是遇到空格、换行、eof 为止。 +- `long long readLong()`:读入一个 long long。 +- `long long readLong(long long L, long long R)`:同上,但是限定只能读入 $[L, R]$ 范围的整数。$L,R$ 必须是 long long 类型的变量。 +- `int readInt()`:读入一个 int。 +- `int readInt(int L, int R)`:同上,但是限定只能读入 $[L, R]$ 范围的整数。 +- `double readReal()`:读入一个实数。 +- `double readReal(double L, double R)`:同上,但是限定只能读入 $[L, R]$ 范围的实数。 +- `double readStrictReal(double L, double R, int minPrecision, int maxPrecision)`:读入一个限定范围精度位数的实数。 +- `string readString()`、`string readLine()`:碰撞一行 string,到换行或者 eof 为止。 +- `void readEoln()`:读入一个换行符。 +- `void readEof()`:读入一个 eof。 +- `void skipBlanks()`:跳过空白字符,跳到下一个非空白字符。如果需要对输入格式做严格的换行校验,但是不在意输出行末空格的话,务必先使用 `skipBlanks` 再 `readEoln`,否则对用户输出格式的要求可能将过于苛刻。 +- `bool seekEof()`:跳过空白字符后,返回是否读到了 eof。 - if (fabs(pans - jans)>0.01) - quitf(_wa, "The answer is wrong: expected = %f, found = %f", jans, pans); +当读入完毕测试数据后,即可和其他正常程序一样,通过编写 C++ 代码,对选手的输出进行校验,最后需要返回评测结果,类似于 printf 的使用方式: - } - quitf(_ok, "The answer is correct."); - return 0; +- `quitf(_ok, "The answer is correct. answer is %d", ans);`:返回 AC。 +- `quitf(_wa, "The answer is wrong: expected = %f, found = %f", jans, pans);`:返回 WA。 +- `quitp(0.5,"Partially Correct get %d percent", 50);`:给出 50% 的部分分。 -} -``` +testlib.h 要求程序必须以 quitf 或者 quitp 结束,不允许没有返回评测结果的情况。 -以下读入命令可以使用: +### 常用案例 -`void registerTestlibCmd(argc, argv)` +接下来,提供 testlib.h 官方提供的若干个案例,它们可以用于你进一步了解 testlib.h 的使用,也可以直接用于试题中。 -初始化 checker,必须在最前面调用一次。 +#### 浮点数比较 -`char readChar()` +该文件可以用于任意数量的浮点数判断。该程序用于判断两个浮点数的绝对误差或者相对误差是否小于 $\epsilon$。换句话说,如果你的程序输出 $a$ 而正确值为 $b$,当满足以下条件时答案被接受: -读入一个 char,指针后移一位。 +$$ +\frac{|a - b|}{\max(1, b)} \le \epsilon +$$ -`char readChar(char c)` +```cpp +#include "testlib.h" -和上面一样,但是只能读到一个字母 c +using namespace std; -`char readSpace()` +const double EPS = 1E-6; -同 readChar(' '). +int main(int argc, char *argv[]) { + setName("compare two sequences of doubles, max absolute or relative error = %.7f", EPS); + registerTestlibCmd(argc, argv); -`string readToken()` + int n = 0; + double j = 0, p = 0; + + while (!ans.seekEof()) { + n++; + j = ans.readDouble(); + p = ouf.readDouble(); + if (!doubleCompare(j, p, EPS)) { + quitf(_wa, "%d%s numbers differ - expected: '%.7f', found: '%.7f', error = '%.7f'", + n, englishEnding(n).c_str(), j, p, doubleDelta(j, p)); + } + } -读入一个字符串,但是遇到空格、换行、eof 为止、 + if (n == 1) + quitf(_ok, "found '%.7f', expected '%.7f', error '%.7f'", p, j, doubleDelta(j, p)); -`long long readLong()` + quitf(_ok, "%d numbers", n); +} +``` -读入一个 longlong/int64 +需要指出,通常推荐使用 testlib.h 自带的 `doubleCompare` 函数进行浮点数比较,而非自己随意实现。`doubleCompare` 传参分别是标准答案、用户答案和容许的相对误差,返回 true 表示用户答案在误差范围内,返回 false 表示用户答案超过误差范围。 -`long long readLong(long long L, long long R)` +#### 判断 Yes/No -同上,但是限定范围(包括 L,R) +该文件可以用于任意数量的 Yes 和 No 判断,且大小写不敏感。例如:如果标准答案是 Yes,那么选手答案是 YEs 也视作正确。 -`int readInt()` +```cpp +#include "testlib.h" +#include -读入一个 int +using namespace std; -`int readInt(int L, int R)`, +const string YES = "YES"; +const string NO = "NO"; -同上,但是限定范围(包括 L,R) +int main(int argc, char *argv[]) { + setName("%s", ("multiple " + YES + "/" + NO + " (case insensitive)").c_str()); + registerTestlibCmd(argc, argv); -`double readReal()` + int index = 0, yesCount = 0, noCount = 0; + string pa; + while (!ans.seekEof() && !ouf.seekEof()) { + index++; + string ja = upperCase(ans.readToken()); + pa = upperCase(ouf.readToken()); + + if (ja != YES && ja != NO) + quitf(_fail, "%s or %s expected in answer, but %s found [%d%s token]", + YES.c_str(), NO.c_str(), compress(ja).c_str(), index, englishEnding(index).c_str()); + + if (pa == YES) + yesCount++; + else if (pa == NO) + noCount++; + else + quitf(_pe, "%s or %s expected, but %s found [%d%s token]", + YES.c_str(), NO.c_str(), compress(pa).c_str(), index, englishEnding(index).c_str()); + + if (ja != pa) + quitf(_wa, "expected %s, found %s [%d%s token]", + compress(ja).c_str(), compress(pa).c_str(), index, englishEnding(index).c_str()); + } -读入一个实数 + int extraInAnsCount = 0; + while (!ans.seekEof()) { + ans.readToken(); + extraInAnsCount++; + } -`double readReal(double L, double R)`, + int extraInOufCount = 0; + while (!ouf.seekEof()) { + ouf.readToken(); + extraInOufCount++; + } -同上,但是限定范围(包括 L,R) + if (extraInAnsCount > 0) + quitf(_wa, "Answer contains longer sequence [length = %d], but output contains %d elements", + index + extraInAnsCount, index); -`double readStrictReal(double L, double R, int minPrecision, int maxPrecision)`, + if (extraInOufCount > 0) + quitf(_wa, "Output contains longer sequence [length = %d], but answer contains %d elements", + index + extraInOufCount, index); -读入一个限定范围精度位数的实数。 + if (index == 0) + quitf(_ok, "Empty output"); + else if (index == 1) + quitf(_ok, "%s", pa.c_str()); + else + quitf(_ok, "%d token(s): yes count is %d, no count is %d", index, yesCount, noCount); -`string readString()`, + quitf(_fail, "Impossible case"); +} +``` -`string readLine()` +#### 针对 Case #%d: 的解析: -碰撞一行 string,到换行或者 eof 为止 +部分 ICPC 试题以及 Google CodeJam 试题会使用形如: + +``` +Case #1: 2 +Case #2: 4 +Case #3: 0 +``` -`void readEoln()` +的形式输出答案。对于此类情形,testlib.h 提供了一个官方的解析模板,可以在此基础上增加需要自定义校验的内容: -读入一个换行符 +```cpp +/** + * Checker to compare output and answer in the form: + * + * Case #1: + * Case #2: + * ... + * Case #n: + * + */ -`void readEof()` +#include "testlib.h" +#include +#include -读入一个 eof +using namespace std; -`int eof()` +vector readStream(InStream &in) { + vector result; -读完数据后,就可以开始 spj 了。选手程序能用的功能,spj 一样能用。在洛谷中,spj 照样受到时间空间限制。而且不能标准输入输出。 + for (int testCase = 1; !in.seekEof(); testCase++) { + string caseStr = in.readToken(); + if (caseStr != "Case") + in.quitf(_pe, "Expected 'Case' but found '%s' [test case %d]", compress(caseStr).c_str(), testCase); -最后就是输出啦。输出跟 printf 有点像。 + string numExpStr = "#" + to_string(testCase) + ":"; + string numStr = in.readToken(); + if (numExpStr != numStr) + in.quitf(_pe, "Expected '%s' but found '%s' [test case %d]", compress(numExpStr).c_str(), + compress(numStr).c_str(), testCase); -`quitf(_ok, "The answer is correct. answer is %d", ans);` + result.push_back(in.readLong()); // 如果一行内有多个整数,可以编辑这里。 + } -给出 AC + return result; +} -`quitf(_wa, "The answer is wrong: expected = %f, found = %f", jans, pans);` +int main(int argc, char *argv[]) { + setName("Single int64 checker with testcase-support"); + registerTestlibCmd(argc, argv); -给出 WA + vector ja = readStream(ans); + vector pa = readStream(ouf); + + for (unsigned int i = 0; i < min(ja.size(), pa.size()); i++) + if (ja[i] != pa[i]) + quitf(_wa, "Expected %s found %s [test case %d]", vtos(ja[i]).c_str(), vtos(pa[i]).c_str(), i + 1); + + if (ja.size() != pa.size()) + quitf(_pe, "Expected %u test case(s) but found %u", (unsigned int) (ja.size()), (unsigned int) (pa.size())); + + string message = format("%u case(s):", (unsigned int) (ja.size())); + if (ja.size() <= 5) { + for (auto elem: ja) + message += " " + vtos(elem); + } else { + for (int i = 0; i < 3; i++) + message += " " + vtos(ja[i]); + message += " ..."; + for (int i = 0; i < 2; i++) + message += " " + vtos(ja[ja.size() - 2 + i]); + } -`quitp(0.5,"Partially Correct get %d percent", 50);` + quitf(_ok, "%s", message.c_str()); +} +``` -给出 PC(Partially Correct),并且可以获得该点 50% 的分数 +可以在这个案例中发现,为了便于处理 `ans` 流和 `ouf` 流,可以编写一个通用的函数。事实上,`ans`、`ouf` 和 `inf` 都是 `InStream` 类型的对象,因此它们可以共享相同的读取处理方法。`readStream` 函数接受的是一个 `InStream` 类型的引用。由于 `InStream` 是一个通用的输入流类,`ans` 和 `ouf` 都可以作为参数传递给该函数,执行相同的读取操作。当需要编写较复杂的处理逻辑时,可以通过这一点优化代码编写。 ## 测试 @@ -168,7 +300,7 @@ checker.exe in.txt out.txt ans.txt (Windows) ::: -然后,就没有然后了。 +由于在线评测时需要编译 checker.cpp 并且执行比较,评测带有 Special Judge 的试题会慢于正常试题。 ## Codeforces 赛制 diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 92ccd33..f2d48fc 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -12,7 +12,6 @@ export default { baseUrl: '/', onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', i18n: { defaultLocale: 'zh-Hans',