Checker
The checker is usually used to check the answers of participants in cases where there are multiple correct answers, or when more detailed scoring requirements are needed.
Checkers usually need to process three files:
- Input file (
inf
): Input for the participant's program. - Output file (
ouf
): Output of the participant's program. - Answer file (
ans
): Usually equivalent to the output of the standard (main correct) program.
Checkers usually need to check two conditions:
- Based on the input file of the test point, determine whether the participant's output is "valid" (i.e., conforms to the output format and constraints).
- Compare the participant's output with the standard answer and check if they are "equivalent" (i.e., logically correct according to problem rules).
Basics
The relevant code for this section can be found in the GitHub repository (opens in a new tab).
Let's start with a simple example to explain how to write and use the checker.
Problem Description: Given two integers and (), you need to output the sum of and .
Implementation
First, start with an empty working directory, copy cplib.hpp
into the working directory, and then create a file named chk.cpp
with the following code:
#include "cplib.hpp"
using namespace cplib;
// Define the Input struct to read problem-specific inputs (A and B for A+B problem)
struct Input {
int a, b;
static Input read(var::Reader& in) {
// Read integer 'a' within range [-1000, 1000]
auto a = in.read(var::i32("a", -1000, 1000));
// Read integer 'b' within range [-1000, 1000]
auto b = in.read(var::i32("b", -1000, 1000));
return {a, b};
}
};
// Define the Output struct to read the answer and evaluate it
struct Output {
int ans;
// Static read method to parse participant/jury output
// It receives a var::Reader and the Input struct for context if needed.
static Output read(var::Reader& in, const Input&) {
// Read integer 'ans' within range [-2000, 2000]
auto ans = in.read(var::i32("ans", -2000, 2000));
return {ans};
}
// Static evaluate method to compare participant's output with jury's output
// It receives an evaluate::Evaluator, participant's output (pans),
// jury's output (jans), and the original Input.
static evaluate::Result evaluate(evaluate::Evaluator& ev, const Output& pans, const Output& jans,
const Input&) {
// Use ev.eq to compare the 'ans' field.
// If pans.ans == jans.ans, it contributes to AC. Otherwise, it marks WA.
auto res = evaluate::Result::ac();
res &= ev.eq("ans", pans.ans, jans.ans);
return res;
}
};
// Register the checker with CPLib, specifying the Input and Output structs.
// This macro sets up the main checker logic behind the scenes.
CPLIB_REGISTER_CHECKER(chk, Input, Output);
We will explain each line of this code step by step.
using namespace cplib;
The main functionalities of CPLib are stored in the namespace cplib
. Since programs used in Competitive Programming are generally short, to a certain extent, using namespace cplib;
greatly optimizes development efficiency.
struct Input { /* ... */ };
The Input
struct encapsulates all data read from the problem's input file. It must contain a static method read
that takes a cplib::var::Reader&
as its first parameter and returns an instance of Input
. This method is responsible for parsing the input file and performing any necessary initial validations on the input data itself.
struct Output { /* ... */ };
The Output
struct encapsulates all data read from a program's output file (both participant's and jury's). It must contain:
- A static method
read
that takes acplib::var::Reader&
as its first parameter, and optionally aconst Input&
as its second parameter (if output parsing depends on input data). It returns an instance ofOutput
. This method is responsible for parsing the output file and performing "validity" checks on the output format and constraints. If the output is invalid (e.g., out of range, wrong format), it should callin.fail("message")
. - A static method
evaluate
that takes anevaluate::Evaluator&
,const Output& pans
(participant's output),const Output& jans
(jury's output), andconst Input& input
as parameters. It returns anevaluate::Result
. This method is responsible for comparingpans
andjans
based on the problem's logical correctness rules.
auto a = in.read(var::i32("a", -1000, 1000));
This line demonstrates reading a variable using a variable reading template. cplib::var::i32
is an alias for cplib::var::Int<int32_t>
. It's a template that specifies how to read a 32-bit signed integer. The constructor parameters ("a", -1000, 1000)
define:
"a"
: The name of the variable. When an error occurs during reading, this name will be displayed in the error message along with the reading stack trace, making error reports more readable.-1000, 1000
: The minimum and maximum allowed values for the integer. CPLib automatically checks if the read value falls within this range.
The constructor declarations for cplib::var::Int<T>
are:
explicit Int();
explicit Int(std::string name);
explicit Int(std::optional<T> min, std::optional<T> max);
explicit Int(std::string name, std::optional<T> min, std::optional<T> max);
While the name
parameter can be omitted , it is strongly discouraged as it makes error reports less readable.
auto res = evaluate::Result::ac();
res &= ev.eq("ans", pans.ans, jans.ans);
return res;
Inside the Output::evaluate
method, evaluate::Evaluator& ev
is used to compare results. ev.eq("ans", pans.ans, jans.ans)
compares the ans
field of the participant's output (pans
) with the jury's output (jans
). If they are equal, it contributes to an "Accepted" result. If they are not equal, it marks the result as "Wrong Answer" and provides a detailed message including the variable name "ans" and the values.
When an output file (participant's or jury's) does not meet the format or range constraints specified in its read
method, CPLib will automatically exit the program and report an error. For participant's output, this results in "Wrong Answer". For jury's output, it results in "Internal Error", indicating a problem with the test data itself.
CPLIB_REGISTER_CHECKER(chk, Input, Output);
The CPLIB_REGISTER_CHECKER(name, InputStruct, OutputStruct)
macro registers the program as a CPLib checker. name
(in the code, chk
) is the name of the global state variable for the checker. InputStruct
and OutputStruct
are the names of the struct
s that define how to read the problem input and how to read and evaluate the program output, respectively. CPLib will internally manage the main
function and call the read
and evaluate
methods of these structs.
Programs registered as CPLib programs must use the evaluate::Evaluator
methods (like ev.eq
, ev.fail
) in evaluate
methods, or in.fail()
in read
methods, to report errors and exit. It is not allowed to directly use functions like std::exit
for normal error reporting. panic
should only be used for unrecoverable internal errors, such as a critical bug in the checker logic itself or an invalid jury's answer that cannot be handled gracefully by ev.fail()
.
Usage
Open the terminal, navigate to the working directory you just created, and execute the compilation command to compile the checker, which is typically similar to this:
g++ chk.cpp -o chk -std=c++20
Next, create a folder named data/
in the working directory to store the test points used by the checker and place the test data inside. In this example, we use data/0.in
, data/0.out
, and data/0.ans
as the names of the test data.
At this point, your working directory structure should look like this:
- 0.ans
- 0.in
- 0.out
Execute the following command in the terminal to test your checker:
./chk data/0.in data/0.out data/0.ans
If everything is normal up to this point, you will see the following results from the checker:
Accepted, scores 100.0 of 100.
If you want to experiment on your own, you can:
- Try changing the content of your test point input file, participant's output file, and standard answer file, and observe the changes in the output of the checker.
- Run
./chk --help
and observe the impact of changing the--report-format
command-line parameter and settingNO_COLOR=1
orCLICOLOR_FORCE=1
environment variables on the output of the checker.
Advanced
The relevant code for this section can be found in the GitHub repository (opens in a new tab).
In actual problems encountered in Competitive Programming, the problems are not as simple as A+B. Let's look at a slightly more complex example.
Problem Description: Given an undirected connected simple graph with nodes () and edges (), where the endpoints of the -th edge are and (), and the edge weight is (). You need to solve two problems: find the sum of the weights of the shortest path from to , and output any one shortest path.
Input Format: The first line contains two integers and . Next, there are lines, each containing three integers .
Output Format: The first line outputs an integer , representing the answer to the first problem. The second line first outputs an integer in the range , representing the number of nodes traversed in the shortest path. Then it outputs integers, representing the node numbers traversed in the shortest path (including the starting node and the ending node ).
When writing a more complex checker, it is recommended to structure the logic using Input
and Output
structs with their respective read
and evaluate
methods. This approach leads to more readable and maintainable implementations.
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "cplib.hpp"
using namespace cplib;
// Define an Edge struct for reading graph edges
struct Edge {
int u, v, w;
// Edge::read takes a Reader and the graph size 'n' as context
static Edge read(var::Reader& in) {
// Using in() operator for reading multiple variables conveniently
auto [u, v, w] = in(var::i32("u"), var::i32("v"), var::i32("w"));
return {u, v, w};
}
};
// Define the Input struct to hold graph data
struct Input {
int n, m;
std::map<std::pair<int, int>, int> graph; // Adjacency map for quick edge lookup
static Input read(var::Reader& in) {
auto [n, m] = in(var::i32("n"), var::i32("m"));
// Read 'm' edges, passing 'n' as context to Edge::read via var::ExtVar
auto edges = in.read(var::ExtVar<Edge>("edges") * m);
std::map<std::pair<int, int>, int> graph;
for (auto [u, v, w] : edges) {
graph[{u, v}] = w;
graph[{v, u}] = w; // Undirected graph
}
return {n, m, graph};
}
};
// Define the Output struct to hold the shortest path sum and the path itself
struct Output {
int sum, len;
std::vector<int> plan;
// Output::read takes Reader and the Input struct as context
static Output read(var::Reader& in, const Input& input) {
// Read sum and length, with appropriate ranges
auto sum = in.read(var::i32("sum", 0, std::nullopt));
auto len = in.read(var::i32("len", 1, input.n * input.n)); // Max path length for N nodes
// Read the path plan, ensuring nodes are within [1, input.n]
auto plan = in.read(var::i32("plan", 1, input.n) * len);
// Perform validity checks on the path
if (plan.empty()) in.fail("Path cannot be empty");
if (plan.front() != 1) in.fail("Path should begin with 1");
if (plan.back() != input.n) in.fail("Path should end with n");
int result_sum = 0;
for (int i = 1; i < (int)plan.size(); ++i) {
if (!input.graph.count({plan[i - 1], plan[i]}))
in.fail(format("Edge {} <-> {} does not exist", plan[i - 1], plan[i]));
result_sum += input.graph.at({plan[i - 1], plan[i]});
}
if (result_sum != sum)
in.fail(format("Calculated path sum ({}) from plan does not match reported sum ({})",
result_sum, sum));
return {sum, len, plan};
}
// Output::evaluate compares participant's output with jury's output
static evaluate::Result evaluate(evaluate::Evaluator& ev, const Output& pans, const Output& jans,
const Input&) {
auto res = evaluate::Result::ac();
// If participant's sum is strictly less than jury's sum,
// it implies the jury's answer is not optimal. This is an internal error.
if (pans.sum < jans.sum) {
ev.fail(
format("Participant's path sum ({}) is less than jury's path sum ({})! This indicates a "
"judge error.",
pans.sum, jans.sum));
}
// The problem asks for the shortest path sum.
// If participant's sum is greater than jury's, it's WA.
res &= ev.eq("sum", pans.sum, jans.sum);
// If sums are equal, the path itself needs to be valid (already checked in read)
// and correctly connects 1 to n (also checked in read).
// No further comparison of 'plan' is strictly needed if 'sum' is correct,
// as any valid path yielding the shortest sum is acceptable.
// However, for completeness or stricter checks, one might compare 'len' or 'plan'
// if multiple shortest paths exist and specific ones are preferred.
// For this problem, we only care about the sum.
return res;
}
};
// Register the checker
CPLIB_REGISTER_CHECKER(chk, Input, Output);
This example demonstrates how to use variable reading templates for custom types and how to structure the evaluation logic.
Structured Data Types and read
Methods:
Edge
struct: Defines the structure for a single edge. Itsstatic read
method readsu
,v
, andw
.Input
struct: Encapsulatesn
,m
, and the graph adjacency map. Itsstatic read
method readsn
andm
, then usesvar::ExtVar<Edge>("edges") * m
to readm
edges.var::ExtVar<T>
is used to create a variable reading template for a custom typeT
. WhenT::read
requires additional parameters, these parameters are passed to thevar::ExtVar<T>
constructor. For types like theEdge
type in this example, whoseEdge::read
method has only one parameter, the constructor does not need to pass any parameters withoutname
. Theoperator()
ofin
(e.g.,in(var::i32("n"), var::i32("m"))
) provides a concise way to read multiple variables with no interdependencies into astd::tuple
.Output
struct: Holds the reported sum, path length, and the path itself. Itsstatic read
method performs crucial "validity" checks:- Ensuring path nodes are within
[1, input.n]
. - Verifying the path starts at
1
and ends atn
. - Checking if all edges in the path exist in the graph.
- Confirming that the reported
sum
matches the sum calculated from theplan
. - If any validity check fails,
in.fail(message)
is called, which immediately reports "Wrong Answer" (for participant's output) or "Internal Error" (for jury's output) with a detailed stack trace.
- Ensuring path nodes are within
Evaluation with evaluate::Evaluator
:
The Output::evaluate
method is where the core "equivalence" check happens.
- It receives an
evaluate::Evaluator& ev
, which is the primary tool for reporting results. - A special case is handled: if
pans.sum < jans.sum
, it means the participant found a better answer than the jury's. This indicates a problem with the jury's solution or test data.ev.fail()
is used here to report an "Internal Error" with a specific message, prompting a review of the jury's answer. This is a robust way to handle such scenarios, distinguishing them from a regular "Wrong Answer". ev.eq("sum", pans.sum, jans.sum)
checks if the participant's sum (pans.sum
) is equal to the jury's sum (jans.sum
). Ifpans.sum > jans.sum
, it automatically marks "Wrong Answer" with a descriptive message.
Suggestions
In summary, when writing more complex checkers, we recommend the following suggestions:
- Structure your code: Use custom
Input
andOutput
structs with theirstatic read
andstatic evaluate
methods. This modular design improves readability and maintainability. - Leverage
var::Var
: Usecplib::var::ExtVar
for custom types andoperator*
for vectors. Theoperator()
ofin
can simplify reading multiple independent variables. - Perform "validity" checks in
read
methods: Implement checks for output format, ranges, and basic consistency (e.g., path starts/ends correctly, edges exist) within thestatic read
methods of yourOutput
struct and its nested types. Usein.fail("message")
to report these errors. - Perform "equivalence" checks in
evaluate
methods: Implement the core logical comparison between participant's and jury's outputs in thestatic evaluate
method of yourOutput
struct. Useevaluate::Evaluator
methods (likeev.eq
,ev.approx
,ev.fail
) for robust and descriptive result reporting.
If you want to experiment on your own, you can:
Try writing a checker for the following problem.
Problem Description: For a graph with () nodes and () undirected edges, output the number of edge biconnected components and output each edge biconnected component.
Input Format: The first line contains two integers and . Next, there are lines, each containing two integers , representing an undirected edge.
Output Format: The first line contains an integer representing the number of edge biconnected components. The following lines each start with an integer representing the number of nodes in the component, followed by integers describing an edge biconnected component. You can output the edge biconnected components and the nodes within them in any order.
For the example implementation, see the GitHub repository (opens in a new tab).