Wiki Hướng dẫn ra đề Trình chấm tùy chỉnh (Custom Checker)

Trình chấm tùy chỉnh (Custom Checker)

huunguyen huunguyen Updated Tháng tư 17, 2026

Khi nào cần custom checker?

Mặc định, CTOJ so sánh output của thí sinh với output chuẩn từng ký tự (bỏ qua khoảng trắng thừa cuối dòng). Điều này hoạt động tốt cho hầu hết bài toán. Tuy nhiên, bạn cần custom checker khi:

  • Nhiều đáp án đúng: Bài yêu cầu "in một hoán vị thỏa điều kiện" — có thể có nhiều hoán vị đúng khác nhau.
  • Sai số số thực: Bài yêu cầu "in kết quả với sai số 106" — cần so sánh với tolerance.
  • Output phụ thuộc vào input: Cần đọc input gốc để kiểm tra tính đúng đắn (ví dụ: kiểm tra output có phải là cây khung nhỏ nhất không).
  • Định dạng linh hoạt: Chấp nhận nhiều cách trình bày khác nhau cho cùng một đáp án.

Loại checker trên CTOJ

CTOJ hỗ trợ checker kiểu bridged — sử dụng thư viện testlib.h. Đây là chuẩn phổ biến nhất trong competitive programming (Codeforces, Polygon, ...).

Khi tạo bài, thiết lập:

  • checker = "bridged"
  • custom_checker_source = mã nguồn C++ của checker

Cấu trúc checker testlib

#include "testlib.h"
using namespace std;

int main(int argc, char* argv[]) {
    registerTestlibCmd(argc, argv);

    // Đọc dữ liệu từ 3 stream:
    // inf  — file input gốc (.in)
    // ouf  — output của thí sinh
    // ans  — output chuẩn (.out)

    // Kiểm tra và trả verdict
    quitf(_ok, "correct");   // AC
    quitf(_wa, "wrong");     // WA
}
Ba stream quan trọng
Stream Ý nghĩa Dùng khi
inf File input gốc (.in) Đọc n, mảng, đồ thị... để kiểm tra output có hợp lệ không
ouf Output của thí sinh Luôn dùng — đây là cái cần kiểm tra
ans Output chuẩn (.out) Đọc đáp án mẫu nếu cần so sánh (ví dụ: so giá trị tối ưu)
Các hàm đọc phổ biến
int n = inf.readInt();                    // đọc một int
long long x = inf.readLong();             // đọc một long long
double d = ouf.readDouble();              // đọc một double
string s = ouf.readToken();               // đọc một token (không khoảng trắng)
string line = ouf.readLine();             // đọc cả dòng
int v = ouf.readInt(1, n, "vertex");      // đọc int trong [1, n], tên biến cho thông báo lỗi
Các verdict
quitf(_ok, "message");     // Accepted
quitf(_wa, "message");     // Wrong Answer
quitf(_pe, "message");     // Presentation Error (hiếm dùng)
quitf(_fail, "message");   // Judge Error — dùng khi phát hiện lỗi trong test/checker

Ví dụ 1: Bài có nhiều đáp án đúng

Bài toán: Cho chuỗi s, sắp xếp lại các ký tự sao cho không có hai ký tự liền kề giống nhau. In IMPOSSIBLE nếu không thể.

#include "testlib.h"
#include <string>
#include <map>
#include <algorithm>
using namespace std;

int main(int argc, char* argv[]) {
    registerTestlibCmd(argc, argv);

    int n = inf.readInt();
    string s = inf.readToken();

    string out = ouf.readToken();

    // Kiểm tra có khả thi không
    map<char,int> cnt;
    for (char c : s) cnt[c]++;
    int maxf = 0;
    for (auto& [k, v] : cnt) maxf = max(maxf, v);
    bool possible = maxf <= (n + 1) / 2;

    // Xử lý IMPOSSIBLE
    string up = out;
    for (auto& ch : up) ch = toupper(ch);

    if (up == "IMPOSSIBLE") {
        if (possible)
            quitf(_wa, "printed IMPOSSIBLE but a valid arrangement exists");
        quitf(_ok, "correctly reported infeasible");
    }

    if (!possible)
        quitf(_wa, "no valid arrangement exists but contestant printed %s",
              out.c_str());

    // Kiểm tra output hợp lệ
    if ((int)out.length() != n)
        quitf(_wa, "output length %d != expected %d",
              (int)out.length(), n);

    map<char,int> ac;
    for (char c : out) ac[c]++;
    if (ac != cnt)
        quitf(_wa, "output is not a permutation of input");

    for (int i = 1; i < n; i++)
        if (out[i] == out[i-1])
            quitf(_wa, "adjacent duplicate at position %d", i);

    quitf(_ok, "valid rearrangement");
}

Giải thích: Checker đọc chuỗi gốc từ inf, đọc output thí sinh từ ouf, rồi kiểm tra:

  1. Nếu thí sinh in IMPOSSIBLE → kiểm tra có thực sự bất khả thi không.
  2. Nếu thí sinh in kết quả → kiểm tra là hoán vị của chuỗi gốc và không có ký tự liền kề giống nhau.

Ví dụ 2: So sánh số thực với sai số

Bài toán: Tính khoảng cách ngắn nhất giữa hai điểm. Chấp nhận sai số 106.

#include "testlib.h"
#include <cmath>
using namespace std;

int main(int argc, char* argv[]) {
    registerTestlibCmd(argc, argv);

    double expected = ans.readDouble();
    double got = ouf.readDouble();

    // So sánh với sai số tuyệt đối và tương đối
    if (abs(expected - got) <= 1e-6 ||
        abs(expected - got) <= 1e-6 * abs(expected)) {
        quitf(_ok, "answer %.10f, expected %.10f", got, expected);
    }

    quitf(_wa, "answer %.10f, expected %.10f, diff %.10f",
          got, expected, abs(expected - got));
}

Ví dụ 3: Kiểm tra giá trị tối ưu + output hợp lệ

Bài toán: Tìm đường đi ngắn nhất trong đồ thị. In độ dài và đường đi.

#include "testlib.h"
#include <vector>
using namespace std;

int main(int argc, char* argv[]) {
    registerTestlibCmd(argc, argv);

    int n = inf.readInt();
    int m = inf.readInt();

    // Đọc đồ thị từ input
    vector<vector<pair<int,int>>> adj(n + 1);
    for (int i = 0; i < m; i++) {
        int u = inf.readInt(), v = inf.readInt(), w = inf.readInt();
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    long long expected_dist = ans.readLong();
    long long got_dist = ouf.readLong();

    // Kiểm tra giá trị tối ưu
    if (got_dist != expected_dist)
        quitf(_wa, "distance %lld != expected %lld", got_dist, expected_dist);

    // Kiểm tra đường đi hợp lệ
    int k = ouf.readInt();
    vector<int> path(k);
    for (int i = 0; i < k; i++)
        path[i] = ouf.readInt(1, n, "node");

    if (path[0] != 1 || path[k-1] != n)
        quitf(_wa, "path must start at 1 and end at %d", n);

    long long total = 0;
    for (int i = 1; i < k; i++) {
        bool found = false;
        for (auto [v, w] : adj[path[i-1]]) {
            if (v == path[i]) {
                total += w;
                found = true;
                break;
            }
        }
        if (!found)
            quitf(_wa, "no edge between %d and %d", path[i-1], path[i]);
    }

    if (total != got_dist)
        quitf(_wa, "path cost %lld != claimed distance %lld", total, got_dist);

    quitf(_ok, "valid shortest path of length %lld", got_dist);
}

Lưu ý khi viết checker

Dùng _fail cho lỗi judge

Nếu checker phát hiện dữ liệu test bị hỏng (không phải lỗi thí sinh), dùng _fail:

if ((int)s.length() != n)
    quitf(_fail, "judge input malformed: string length %d != n=%d",
          (int)s.length(), n);

Verdict _fail sẽ hiện thành Internal Error (IE) — cho biết lỗi thuộc về hệ thống, không phải thí sinh.

Đọc hết output thí sinh

Nếu thí sinh in thừa dữ liệu, checker nên phát hiện:

// Sau khi đọc xong output cần thiết
if (ouf.seekEof()) {
    quitf(_ok, "correct");
} else {
    quitf(_wa, "extra output after answer");
}

Tuy nhiên, trong thực tế nhiều checker bỏ qua bước này vì hệ thống thường không penalize output thừa.

Test checker cẩn thận

Trước khi deploy, hãy đảm bảo:

  • Output đúng → AC
  • Output sai (sai giá trị) → WA
  • Output sai (sai format, thiếu dữ liệu) → WA
  • IMPOSSIBLE khi có lời giải → WA
  • Có lời giải khi IMPOSSIBLE → WA
Lưu checker trong problem package

Lưu file checker tại solutions/checker.cpp trong thư mục bài toán, cùng với lời giải mẫu. Điều này giúp theo dõi và cập nhật checker khi cần.

Tóm tắt

Tình huống Cần checker? Loại
Output duy nhất Không Mặc định (so sánh text)
Nhiều đáp án đúng bridged + testlib
Sai số số thực bridged + testlib
Output phụ thuộc input bridged + testlib
Chỉ kiểm tra giá trị tối ưu bridged + testlib
gnatmake 12.2.0 a68g 3.1.2 nasm 2.16.1 as_x64 2.46 awk 1.3.4 gcc 16.1.0 csc 6.12.0.200 g++ 16.1.0 g++-themis 16.1.0 g++17 16.1.0 g++20 16.1.0 g++23 16.1.0 clang++ 22.1.6 dmd 2.112.0 dart 3.12.1 gforth 0.7.3 gfortran 12.2.0 go 1.26.3 groovyc 5.0.6 javac 25.0.3 node 26.2.0 kotlinc 2.3.21 sbcl 2.2.9 lua 5.4.8 nim 2.2.10 fpc 3.2.2 fpc-themis 3.2.2 perl 5.36.0 php 8.5.6 pike 8.0 pypy3 7.3.23 python3 3.14.5 racket 8.7 ruby 4.0.5 rustc 1.96.0 csc 5.3.0 ctoj-scratch 0.0.1 sed 4.9 tclsh 8.6 bun 1.3.14 deno 2.8.1 v 0.5.1 zig 0.16.0