交互造题指南

写在前面

​ 这篇文章写在联合省选 2022 两天后。笔者此前造交互题时发现全网都搜不到好上手的教程。笔者一想,自己进集训队的可能性为 \(0\),打算写一篇博客,在退役之前留下真正有用的东西。

NOI Style

​ NOI 风格的交互题就是通过函数调用实现交互。我们假设题目的名字叫做 "test",那么通常一个交互系统中需要这样的几个文件:

  • test.cpp:选手的源代码。
  • grader.cpp:和 test.cpp 交互并判断是否正确的程序。这个程序将下发一份示例版本给选手供其测试自己的程序。当然,为了避免选手从下发的示例版本中发现漏洞并攻击评测,评测时使用的版本应与下发的示例版本(例如在变量名或实现方法上)有所不同。
  • test.h:是 grader.cpptest.cpp 需包含的头文件,以实现交互。这个程序直接下发给选手,无需改动。
  • checker.cpp:在一些评测环境下,需要这个程序将 grader.cpp 的输出转化为分数。这个程序不需要下发给选手。
under Lemon
  • test.h

    在这个头文件里需要定义所有同时出现在 grader.cpptest.cpp 中的函数(即 test.cpp 可调用 grader.cpp 的函数,和 grader.cpp 可调用 test.cpp 的函数)。一个好的 test.h 通常需要包含尽量少的头文件和命名空间,以尊重不同选手的代码习惯。举例:

    #include <vector>
    #include <string>
    
    void solve(int n);
    std::string query(std::vector<int> v);
    void answer(int x);
    
  • grader.cpp

    在 Lemon 下采用这样的评测机制:设置若干个对应程序正确情况的密钥字符串,由 grader.cpp 评定出选手程序情况后按照对应状态,向输出文件(通常就是 test.out)先输出这个密钥字符串,再输出分数。checker.cpp 根据密钥解密状态得到分数,反馈给 Lemon。如果读取到非密钥的字符串,则判定为答案错误。这样就防止了选手攻击交互库,向输出文件自行输出分数的可能性。

    同样为了安全,grader.cpp 中的所有全局变量应被包装在一个 namespace 中。

    下发给选手的 grader 则不需要文件输入输出、密钥。

    举例:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    namespace GLOBAL{
        const string key = "KEY";
    	int T, flg, n;
        double scr, res;
    }
    
    std::string query(std::vector<int> v){ /* ... */ }
    
    void answer(int x){ /* ... */ }
    
    int main(){
    	freopen("test.in", "r", stdin);
        freopen("test.out", "w", stdout);
        
        GLOBAL::scr = 100;
        
        for(scanf("%d", &T); T--; ){
            scanf("%d", &GLOBAL::n);
            
            GLOBAL::res = 100;
            GLOBAL::flg = false;
            
            solve();
            
        	if(!flg) GLOBAL::scr = 0;
            GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res);
        }
        
        printf("%s\n", key);
        printf("%Lf\n", scr);
        
        return 0;
    }
    
  • checker.cpp

    在 Lemon 下评测时首先需要包含头文件 testlib.h(Lemon 版的,帮助 - 使用手册 告诉我们 testlib for lemon 从 Lemon 的源代码仓库中获得)并把 testlib.h 放到和 checker.cpp 同一目录下,并调用 registerLemonChecker(argc, argv); 这句话完成初始化。

    testlib - OI Wiki 这个页面有 testlib 详细的使用方法。概括地说,只需要从 inf(输入文件),ouf(选手输出),ans(参考输出)这几个对象中读取评判正确所需的信息,然后调用 quitf(verdict, message, ...)quitp(score, message, ...) (两个用法和 printf( ... ) 大致相同的函数)返回 Lemon 可识别的状态。

    更有针对性地说,对于交互题而言,采用上述的方法,通常先从 ouf 中读取 grader 输出的密钥,只有当密钥正确时才继续读取分数(或其它信息);否则直接 quitf(_wa, ...)。根据 ouf 中的后续信息,通常判断出正确、错误、部分正确这几种状态。正确则 quitf(_ok, ...);错误则 quitf(_wa, ...);部分正确则 quitp(score, ...)这里 score 需要对该测试点的满分作标准化,即对读取到的 \([0, 100]\) 间的数乘 \(\dfrac{\texttt{perfectScore}}{100}\) 的系数,注意:使用 _pc(score) 的写法是错误的!)。

    举例:

    #include "testlib.h"
    #include <bits/stdc++.h>
    
    using namespace std;
    
    const string key = "KEY";
    
    int main(int argc, char* argv[]) {
      registerLemonChecker(argc, argv);
      std::string sout = ouf.readLine(), sans = ans.readLine();
      if (sout != key) quitf(_wa, "%s", sout.c_str());
      int score = ouf.readInt();
      if(score == 100) quitf(_ok, "ok");
      else quitp(score / 100 * perfectScore, "partially correct");
      return 0;
    }
    
  • Lemon 中的配置

    L5DqZ6.png

    如图所示。

under LOJ(旧版)

​ 旧版 LOJ 处理 NOI 风格交互题,笔者只会一种鸡贼的办法。思路是将这道题设置为传统题 - special judge,并通过 special judge 充当评分的角色。而 grader.cpp 中的内容将会被封装到 test.h 中。(根据合理的推测,将题目设置为交互题,并同样将 grader.cpp 中的内容封装到 test.h 中,且使用下一节中的方法编写 data.yml,也可能成功实现。但笔者此时缺少验证的条件。)下面详细介绍所需要的每一个文件。

  • test.h

    事实上,只需要将原先评测用的 grader.cpp 中的内容全部粘贴在 test.h 的末尾。如果在设置时选择标准输入输出,则这里也不需要文件输入输出。

    此时为了保证安全,grader.cpp 中复制过来的内容也应包含尽量少的头文件和命名空间。

    举例:

    #include <vector>
    #include <string>
    
    void solve(int n);
    std::string query(std::vector<int> v);
    void answer(int x);
    
    namespace GLOBAL{
        const string key = "KEY";
    	int T, flg, n;
        double scr, res;
    }
    
    std::string query(std::vector<int> v){ /* ... */ }
    
    void answer(int x){ /* ... */ }
    
    int main(){
        GLOBAL::scr = 100;
        
        for(scanf("%d", &T); T--; ){
            scanf("%d", &GLOBAL::n);
            
            GLOBAL::res = 100;
            GLOBAL::flg = false;
            
            solve();
            
        	if(!flg) GLOBAL::scr = 0;
            GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res);
        }
        
        printf("%s\n", key);
        printf("%Lf\n", scr);
        
        return 0;
    }
    
  • checker.cpp

    和上一节中 checker.cpp 的写法是几乎一致的,除了 testlib.h 的使用方法略有差异。返回部分正确的分数时需要 quitf(_pc(score), ...)

    举例:

    #include "testlib.h"
    #include <bits/stdc++.h>
    
    using namespace std;
    
    const string key = "KEY";
    
    int main(int argc, char* argv[]) {
      registerTestlibCmd(argc, argv);
      std::string sout = ouf.readLine(), sans = ans.readLine();
      if (sout != key) quitf(_wa, "%s", sout.c_str());
      int score = ouf.readInt();
      if(score == 100) quitf(_ok, "ok");
      else quitp(_pc(score), "partially correct");
      return 0;
    }
    
  • data.yml

    这是专门为 LOJ 下评测而生的最关键的文件。通过该文件彻底设置所有评测细节。需要写好 data.yml 并上传到评测数据当中。

    第一部分是子任务设置:

    subtasks:
      - score: 50
        type: min
        cases: ['1-1','1-2','1-3','1-4']
      - score: 50
        type: min
        cases: ['2-1','2-2','2-3','2-4']
    

    第二部分是上传的测试数据的后缀:注意交互题不需要参考输出,但是为了正确识别,必须创建空的输出文件(例如 1-1.out)上传。

    inputFile: '#.in'
    outputFile: '#.out'
    

    第三部分描述头文件:

    extraSourceFiles:
      - language: cpp
        files:
         - name: test.h
           dest: test.h
      - language: cpp11
        files:
         - name: test.h
           dest: test.h
      - language: cpp17
        files:
         - name: test.h
           dest: test.h
      - language: cpp11-clang
        files:
         - name: test.h
           dest: test.h
      - language: cpp17-clang
        files:
         - name: test.h
           dest: test.h
      - language: c
        files:
          - name: test.h
            dest: test.h
      - language: cpp14-noi
        files:
          - name: test.h
            dest: test.h
    

    第四部分是 SPJ:

    specialJudge:
      language: cpp11
      fileName: checker.cpp
    

    把这些拼起来即可。

  • LOJ 配置

    题目类型选择传统,评测方式选择 Special Judge,输入输出可以选择标准输入输出或者文件输入输出,但必须和 test.h 中的实现相符。将 checker.cpptest.h 和所有的输入输出文件(.in.out)上传到测试数据中。即可。

under LOJ(新版)

​ 鉴于笔者没有这样的经验,这里提供链接 评测设置 #6 - LibreOJ,相信读者一定可以参考该题目学会。

CF Style

​ CF 风格的交互题就是通过标准输入输出流实现交互。很遗憾 Lemon Lime 并不支持这样的交互方式,因此以下仅针对旧版 LOJ 进行说明。(Polygon?没听说过) 在这套系统中需要 test.cpp(选手提交),grader.cppdata.yml

  • grader.cpp

    旧版的 LOJ 的交互从 input 文件(没有后缀名) 中读取输入数据,输出到 score.txt 中(由于文件名是公开不变的,密钥就变得格外重要。)

    输出的分数是 \([0, 100]\) 间的。

    举例:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    namespace GLOBAL{
        const string key = "KEY";
    	int T, flg, n;
        double scr, res;
    }
    
    std::string query(std::vector<int> v){ /* ... */ }
    
    void answer(int x){ /* ... */ }
    
    int main(){
    	freopen("input", "r", stdin);
        freopen("score.txt", "w", stdout);
        
        GLOBAL::scr = 100;
        
        for(scanf("%d", &T); T--; ){
            scanf("%d", &GLOBAL::n);
            
            GLOBAL::res = 100;
            GLOBAL::flg = false;
            
            solve();
            
        	if(!flg) GLOBAL::scr = 0;
            GLOBAL::scr = min(GLOBAL::scr, GLOBAL::res);
        }
        
        printf("%s\n", key);
        printf("%Lf\n", scr);
        
        return 0;
    }
    
  • data.yml

    和上一节中没有太大的区别。

    举例:

    interactor:
      language: cpp11
      fileName: grader.cpp
    subtasks:
      - score: 50
        type: min
        cases: ['1-1', '1-2']
      - score: 50
        type: min
        cases: ['2-1', '2-2']
    inputFile: '#.in'
    
  • LOJ 设置

    设置为交互题,把这两个文件传上去就行了。

posted @ 2022-04-24 22:49  Mackerel_Pike  阅读(301)  评论(0编辑  收藏  举报