testlib 사용하기
개요
testlib는 Polygon에서 프로그래밍 문제를 만들기 위한 유틸리티를 한데 모아놓은 라이브러리입니다. Generator, validator, checker 및 interactor 제작에 공통적으로 사용되며, 신뢰도 높은 랜덤 함수, 안정성 있으며 정규표현식을 지원하는 문자열 파싱, Polygon 및 채점 시스템과의 상호작용 등의 다양한 기능을 갖추고 있으며 Polygon과 GitHub 페이지에서 다양한 사용 예제들도 제공해 줍니다. 문제 출제를 위한 플랫폼 - Polygon 사용하기 글에 간단한 사용 예시가 나와있습니다.
유용한 기능을 많이 갖추고 있고 코드의 주석 또한 훌륭한 반면에 testlib에 대한 별도의 문서화는 이루어지지 않았습니다. 그래서 문제 출제를 위해 Polygon을 사용할 때 testlib에 적응하는 데에 어려움을 겪기 쉽습니다. 이 글에서는 testlib의 기본적인 사용법에 대한 튜토리얼을 제공하고, 유용하게 사용할 수 있는 기능들을 일부 살펴보고자 합니다.
주요 기능
testlib에서 할 수 있는 주요 기능으로는 다음과 같은 것들이 있습니다.
- Generator, validator, checker 및 interator의 등록 (register)
- 명령줄에서 전달한 인자에 따라 답이 결정되는 랜덤1
- 프로그램의 출력 문자열에 대한 유효성 검사 (서식, 자료형, 범위 등)
- 결과에 대한 구체적인 메시지를 채점 시스템에 전달
또한 기본적으로 제공되는 예제 중에는 실제 문제에서 자주 사용되는 케이스들이 많이 있는데, 특히 checker 예제들의 경우에는 Polygon 자체에서 이들을 언제나 사용할 수 있으며 실제로도 스페셜 저지가 아닌 경우에는 이들을 사용하는 것이 안전합니다. Interactive 문제가 아닌 대부분의 경우 실제로 문제를 만들 때 직접 작성해야 하는 코드는 generator와 validator이며, 스페셜 저지가 필요한 경우 checker도 작성해야 합니다. 각각을 만드는 과정을 따라가 보겠습니다.
Generator
먼저 데이터 제작에서 가장 중요한 프로그램인 generator를 만드는 예시입니다. 이 코드는 이전에 개최한 대회인 Codeforces Round #688 (Div. 2)의 A번 문제 Cancel the Trains의 랜덤 제너레이터입니다.
#include "testlib.h"
using namespace std;
const int MAX_T = 100;
const int MAX_N = 100;
const int MAX_COORD = 100;
struct TC {
int n, m;
vector<int> a, b;
};
TC make_tc(int max_n, int max_m, string weighted)
{
TC cur;
if (weighted == "yes")
{
cur.n = rnd.wnext(1, max_n, max_n / 5 + 1);
cur.m = rnd.wnext(1, max_m, max_m / 5 + 1);
}
else
{
cur.n = rnd.next(1, max_n);
cur.m = rnd.next(1, max_m);
}
set<int> sa, sb;
for (int i = 1; i <= cur.n; i++)
{
int x = rnd.next(1, MAX_COORD);
if (sa.find(x) != sa.end())
{
i--;
continue;
}
cur.a.push_back(x);
sa.insert(x);
}
for (int i = 1; i <= cur.m; i++)
{
int x = rnd.next(1, MAX_COORD);
if (sb.find(x) != sb.end())
{
i--;
continue;
}
cur.b.push_back(x);
sb.insert(x);
}
sort(cur.a.begin(), cur.a.end());
sort(cur.b.begin(), cur.b.end());
return cur;
}
int main(int argc, char *argv[])
{
registerGen(argc, argv, 1);
int t = opt<int>("t");
int max_n = opt<int>("max_n");
int max_m = opt<int>("max_m");
string weighted = opt("weighted");
ensuref(1 <= t && t <= MAX_T, "t must be in [1,%d]", MAX_T);
ensuref(1 <= max_n && max_n <= MAX_N, "max_n must be in [1,%d]", MAX_N);
ensuref(1 <= max_m && max_m <= MAX_N, "max_m must be in [1,%d]", MAX_N);
ensuref(weighted == "yes" || weighted == "no", "weighted == {yes, no}");
vector<TC> tests;
for (int tc = 1; tc <= t; tc++)
tests.push_back(make_tc(max_n, max_m, weighted));
cout << t << '\n';
for (int tc = 0; tc < t; tc++)
{
TC cur = tests[tc];
cout << cur.n << ' ' << cur.m << '\n';
for (int i = 0; i < cur.n; i++)
cout << cur.a[i] << (i == cur.n - 1 ? '\n' : ' ');
for (int i = 0; i < cur.m; i++)
cout << cur.b[i] << (i == cur.m - 1 ? '\n' : ' ');
}
}
이 코드를 단계적으로 따라가며 분석해 보겠습니다.
Generator의 등록: registerGen
testlib를 사용하는 모든 프로그램은 항상 처음에 testlib.h
를 include 하며 시작합니다. 이 헤더 파일 내부에는 기본적인 표준 라이브러리들을 include 하는 문장이 다수 포함되어 있어, 사실상 이 헤더 파일 하나만 include 하더라도 다른 표준 라이브러리들을 사용하는 데에 크게 지장이 없습니다.
우선 프로그램이 시작되면 이 프로그램이 generator라는 사실과 프로그램을 실행할 때의 인자가 무엇이었는지, 사용할 랜덤 함수의 버전이 무엇인지를 testlib에 알려야 합니다. 이를 수행하는 것이 registerGen
함수입니다.
int main(int argc, char *argv[])
{
registerGen(argc, argv, 1);
...
}
전달하는 인자로는 main
함수에 전달된 argc
와 argv
를 그대로 전달하면 되며, 랜덤 함수의 버전으로는 항상 1을 사용하는 것이 추천됩니다. 이 인자들을 통해 이후 testlib를 통해 호출할 랜덤 함수에서 나오는 값이 결정되므로 반드시 필요한 작업입니다.
인자들의 파싱: opt
Generator 프로그램은 하나더라도 데이터의 구체적인 성질을 달리하여 만들고 싶은 경우가 있을 것입니다. 예를 들어 대략적으로는 같은 랜덤이지만 특정 데이터에서는 $n$이 작은 것을 테스트하고 싶거나, 테스트 케이스의 수를 많이 넣은 데이터를 만들고 싶을 수도 있습니다. 이를 위해 generator를 사용할 때에는 명령줄에 보다 알아보기 쉬운 형태로 인자의 형식을 커스터마이징하여 전달하고 이를 testlib를 통해 안정적으로 파싱할 수 있습니다.
int t = opt<int>("t");
int max_n = opt<int>("max_n");
int max_m = opt<int>("max_m");
string weighted = opt("weighted");
opt
는 비교적 최근에 추가된 기능입니다. 이전에는 직접 argv
의 특정 인덱스에 접근하여 atoi
등의 함수를 통해 값을 파싱해와야 했기 때문에 불편하고 인자의 이름을 지어주기도 어려웠지만, 이제는 opt
를 통해 편리하게 인자를 넘겨줄 수 있습니다. 자료형은 기본값인 string
과 int
외에도 char
, long long
, double
등 기본 자료형은 모두 사용할 수 있습니다. 이 코드의 의미는 이 generator는 총 4개의 추가적인 인자를 요구하며 각각의 이름이 t
, max_n
, max_m
, weighted
이고, 앞의 3개는 int
형, 마지막은 일반 문자열이어야 한다는 것을 의미합니다. 명령줄에서 이 generator를 사용하는 예시로는 다음이 있습니다.
gen_random -t=100 -max_n=100 -max_m=100 -weighted=yes
opt
덕분에 명령줄의 의미가 보다 명확하게 와닿습니다. 이 명령줄을 통해 generator가 실행되면 t
, max_n
, max_m
에는 100, weighted
에는 “yes”라는 문자열이 들어가게 됩니다.
유효성 검사: ensure(f)
ensure
또는 ensuref
는 기본적으로 C++ 표준 함수인 assert
와 비슷한 역할을 하지만, 채점 시스템에 메시지를 전달할 수 있고 더 유연한 문자열 포맷팅을 사용할 수 있습니다. ensure
를 사용하면 일반 assert
와 비슷하게 단순 조건 하나만을 전달하고 실패 메시지는 기본값으로 나타나므로, 유효성 검사에 실패했을 때 구체적인 내용을 함께 제공받기 위해서는 ensuref
를 사용하는 것이 좋습니다.
ensuref(1 <= t && t <= MAX_T, "t must be in [1,%d]", MAX_T);
ensuref(1 <= max_n && max_n <= MAX_N, "max_n must be in [1,%d]", MAX_N);
ensuref(1 <= max_m && max_m <= MAX_N, "max_m must be in [1,%d]", MAX_N);
ensuref(weighted == "yes" || weighted == "no", "weighted == {yes, no}");
여기서의 검사는 generator에 전달된 옵션이 문제에서 요구하는 입력의 범위와 일치하는지, 그리고 가중치를 둘 것인지 여부를 결정하는 weighted
옵션이 “yes” 또는 “no” 중 하나로 전달되었는지 등의 간단한 검사를 수행하고 있습니다. 첫 번째 인자로 해당 조건을 주고, 두 번째 이후 인자부터는 printf
를 사용하는 것처럼 서식 문자열과 그 순서에 맞는 추가 인자들을 차례대로 전달하면 됩니다.
가중치 없는 / 있는 랜덤: rnd.next / rnd.wnext
이제 프로그램은 원하는 테스트 케이스의 개수만큼 루프를 돌면서 make_tc
함수를 호출하여 테스트 케이스를 하나씩 만듭니다. 이 함수에서 가장 먼저 하는 일은 우선 원하는 n
, m
의 범위 내에서 실제 n
, m
의 값을 하나 고르는 것입니다.
if (weighted == "yes")
{
cur.n = rnd.wnext(1, max_n, max_n / 5 + 1);
cur.m = rnd.wnext(1, max_m, max_m / 5 + 1);
}
else
{
cur.n = rnd.next(1, max_n);
cur.m = rnd.next(1, max_m);
}
랜덤과 관련된 작업을 할 때에는 rnd
객체를 사용합니다. rnd
에는 처음에 generator를 등록할 때 전달한 인자들에 의해 seed가 초기화되어 있으므로, 같은 인자를 통해 프로그램을 실행하면 rnd
를 통해 생성되는 랜덤 값들 또한 같게 나오게 됩니다.
rnd.next
와 rnd.wnext
에는 다양한 버전이 있습니다. 이 중 가장 많이 쓰이는 것으로는 이 코드에서의 예시와 같이 특정 정수 범위 내에서의 값을 하나 고르는 것으로, rnd.next
에 두 개의 정수를 전달하면 그 사이 닫힌 구간 내에서 하나의 정수를 랜덤으로 골라줍니다. rnd.wnext
의 경우 하나의 정수 인자를 추가로 받으며, 이 인자의 의미는 “그 횟수만큼 해당 범위 내에서 균일 분포로 수를 고르고 그 중 최댓값을 선택한다”는 의미입니다.2 위 코드에서는 weighted
가 “yes”인 경우 1과 max_n
, max_m
사이에서 큰 쪽으로 치우쳐지게끔 n
과 m
을 고르게 되며, weighted
가 “no”인 경우에는 균일 분포로 n
과 m
을 선택하게 됩니다.
또 자주 사용되는 rnd.next
의 버전으로는 첫 수를 0으로 암시적으로 지정하고 하나의 정수만을 전달하는 것도 있습니다. 또한 int
대신 다른 자료형의 수를 전달하여 그 자료형에 맞는 오버로딩 버전을 사용할 수도 있고, 아무 인자 없이 호출하면 [0, 1) 구간에서의 아무 double
값을 하나 뽑아줍니다.
이후 make_tc
함수는 지정한 n
과 m
의 개수만큼 루프를 돌며 1~100 사이의 랜덤 값을 뽑아 데이터를 완성하게 됩니다.
데이터의 출력
완성된 데이터를 출력하는 것은 평범하게 cout
이나 printf
로 하면 됩니다. 문제를 풀 때 누구나 겪게 되는 문제인 느린 endl
은 역시 사용하지 않는 것이 좋습니다.
cout << t << '\n';
for (int tc = 0; tc < t; tc++)
{
TC cur = tests[tc];
cout << cur.n << ' ' << cur.m << '\n';
for (int i = 0; i < cur.n; i++)
cout << cur.a[i] << (i == cur.n - 1 ? '\n' : ' ');
for (int i = 0; i < cur.m; i++)
cout << cur.b[i] << (i == cur.m - 1 ? '\n' : ' ');
}
출력 형식은 문제의 조건과 이후 살펴볼 validator를 통과할 수 있게끔 한치의 오차도 없게 맞춰주어야 합니다. 줄 끝에 불필요한 공백을 삽입하거나, 마지막 줄에 개행이 없는 경우 등이 대표적으로 자주 겪게 되는 오류입니다.
Validator
Validator는 만들어진 데이터가 문제의 조건을 지키는지 여부를 검증해주는 프로그램입니다. Generator가 문제를 만드는 데에 가장 중요한 프로그램이라면, validator는 대회가 망하지 않게 만드는 데에 가장 중요한 프로그램이라고 할 수 있습니다. 아무리 generator에서 미리 열심히 조건을 체크하고 입력 조건을 어기지 않도록 구성하도록 노력하더라도, 실수는 언제나 할 수 있으며 특히 여러 종류의 generator를 만드는 경우 위험성이 더욱 커지기 때문에 견고한 validator를 만드는 것은 매우 중요한 일입니다. 위 generator와 같은 문제에 대한 validator 코드는 다음과 같습니다.
#include "testlib.h"
using namespace std;
const int MAX_T = 100;
const int MAX_N = 100;
const int MAX_COORD = 100;
int main(int argc, char *argv[])
{
registerValidation(argc, argv);
int t = inf.readInt(1, MAX_T, "t");
inf.readEoln();
for (int tc = 1; tc <= t; tc++)
{
setTestCase(tc);
int n = inf.readInt(1, MAX_N, "n");
inf.readSpace();
int m = inf.readInt(1, MAX_N, "m");
inf.readEoln();
int prev = -1;
for (int i = 1; i <= n; i++)
{
int cur = inf.readInt(1, MAX_COORD, format("a[%d]", i));
ensuref(cur > prev, "a[%d]=%d must be greater than a[%d]=%d", i, cur, i - 1, prev);
if (i != n)
inf.readSpace();
else
inf.readEoln();
prev = cur;
}
prev = -1;
for (int i = 1; i <= m; i++)
{
int cur = inf.readInt(1, MAX_COORD, format("b[%d]", i));
ensuref(cur > prev, "b[%d]=%d must be greater than b[%d]=%d", i, cur, i - 1, prev);
if (i != m)
inf.readSpace();
else
inf.readEoln();
prev = cur;
}
}
inf.readEof();
}
Validator 등록하기: registerValidation
Generator를 등록할 때와 마찬가지로 validator도 testlib에 등록해야 합니다. 여기에는 registerValidation
이라는 함수가 사용됩니다.
int main(int argc, char *argv[])
{
registerValidation(argc, argv);
...
}
Validator에서 랜덤은 필요하지 않으므로 세 번째 인자는 없습니다. 대신에 testset, group이나 log file 등의 기능을 이용하기 위해 명령줄은 넘겨줄 수 있는데, 이는 이 글에서 다루지는 않겠습니다.
입력 파일로부터 읽기: inf.read***
Generator에서 출력 내용을 cout
이나 printf
를 통해 stdout
에 써줬던 것과는 달리, validator에서 이 파일의 내용을 읽는 데에는 cin
이나 scanf
등 stdin
을 직접 사용하는 것이 아니라 inf
라는 전역 객체를 사용하게 됩니다. 위에서 validator를 등록할 때 이 객체에 stdin
으로부터의 모든 입력 내용이 들어가기 때문입니다.
출력할 때와 달리 입력 과정에서 inf
와 같은 자체 입력 스트림을 사용하는 이유는 출력은 포맷을 지정하여 정확하게 원하는 문자열을 만들어내는 것이 용이한 반면에 입력 문자열이 원하는 포맷 조건을 지키는지 정밀하게 검사하며 파싱하는 것은 표준 라이브러리들만으로는 어려운 일이며 파싱 에러가 발생했을 때 즉각 대처하기도 까다롭기 때문입니다. 따라서 validator에서는 inf
에서 제공하는 다양한 입력 함수들을 활용하여 입력 조건을 철저하게 검사할 필요가 있습니다.
이 코드에서 입력에 사용된 문장들을 일부 살펴보면 다음과 같습니다.
int t = inf.readInt(1, MAX_T, "t");
inf.readEoln();
...
int n = inf.readInt(1, MAX_N, "n");
inf.readSpace();
int m = inf.readInt(1, MAX_N, "m");
inf.readEoln();
...
int cur = inf.readInt(1, MAX_COORD, format("a[%d]", i));
...
inf.readEof();
우선 일반적으로 가장 자주 쓰일 함수는 inf.readInt
입니다. 이 함수는 기본적으로 두 개의 인자를 요구하며, 이는 위에서 rnd.next
에 쓰인 것과 비슷하게 허용할 수의 범위를 나타냅니다. 따라서 inf.readInt(1, MAX_T, "t");
는 [1,MAX_T]의 범위에서 int
형 정수를 하나 읽어온다는 것을 의미합니다. inf
의 입력 함수들에는 마지막에 문자열을 하나 전달하게끔 되어있는데, 이는 그 입력이 무엇을 나타내는 것인지를 알리기 위한 것으로 선택사항이지만 Polygon에서는 디버깅을 위해 강력하게 권장됩니다.3 int
외에 long long
값을 읽고 싶다면 inf.readLong
을 쓸 수 있으며, double
은 inf.readDouble
을 쓰면 됩니다.
Validator의 역할은 입력 파일이 한 글자의 오차도 없이 올바른 형식을 갖추고 있는지를 평가하는 것이므로 공백 하나, 개행 하나도 확실하게 읽고 판단해야 합니다. 공백을 읽기 위해서는 inf.readSpace
를 사용하며, 개행을 읽기 위해서는 inf.readEoln
을 사용합니다. 또한 모든 입력이 끝난 뒤 파일에 여분의 문자가 남아있지 않은지 역시도 확인해야 하며 이때 inf.readEof
를 사용해야 합니다.
문자열을 읽는 경우 정규표현식을 사용하여 특정 패턴의 문자열을 읽게 할 수 있습니다. 위 코드에는 나오지 않았으나, 이전 Codeforces Round #620 (Div. 2)에 사용했던 Longest Palindrome 문제의 validator에 쓰인 다음과 같은 예시가 있습니다.
string s = inf.readString("[a-z]+", format("string %d", i));
이 코드는 inf.readString
을 사용하여 하나의 문자열을 읽으며, 이 문자열은 정규표현식으로 “[a-z]+” 패턴을 가져야 함을 의미합니다. 즉, 하나 이상의 소문자로 이루어진 문자열이 아니면 검증에 실패하게 됩니다.
추가로, 위에서 몇 번 쓰인 format
은 표준 라이브러리인 vsprintf
를 사용하여 포맷팅된 문자열과 인자를 통해 문자열을 생성하고 이를 std::string
형으로 만들어 반환해주는 간편한 함수입니다.
Checker
이제 마지막으로 checker를 만들어야 합니다. 그런데 그 전에, 이 문제가 과연 별도의 checker가 필요한 출력 형식을 가지고 있는지부터 생각해 보겠습니다. 문제에서 요구하는 것은 그저 $t$개의 정수를 한 줄에 하나씩 출력해야 하는 문제이며, 관례상 한 줄에 하나씩이 아니더라도 정답으로 인정을 해줄 것입니다. 또한 답이 여럿이 될 수도 없어 스페셜 저지가 따로 필요하지 않습니다. 이러한 경우에는 checker를 따로 작성할 필요가 없습니다. 기본으로 제공되는 예제를 사용하면 됩니다.
기본 Checker들
이 문제와 같이 단순히 하나 이상의 정수를 출력해야 하는 문제라면 ncmp.cpp
를 사용할 수 있습니다. 이 파일은 일반적으로 C++에서 정수용으로 사용하는 가장 큰 범위인 int64
내의 모든 정수에 대한 처리가 가능합니다.
조금 더 일반적인 문자열에 대해서도 유연하게 사용할 수 있는 파일은 lcmp.cpp
이지만, 이는 반드시 줄 단위로만 검사를 수행하며 정수만을 출력하는 문제에 사용할 경우 메시지에 융통성이 없어진다는 단점이 있습니다.
실수를 출력해야 하는 문제의 경우 일정 범위 내의 오차를 허용하는 것이 일반적입니다. 이 경우에는 오차 범위에 따라 rcmp4.cpp
, rcmp6.cpp
, rcmp9.cpp
를 사용할 수 있습니다. 각각 $10^{-4}$, $10^{-6}$, $10^{-9}$의 오차를 허용하며, testlib.h
에 포함된 doubleCompare
함수를 통해 정교하게 결과를 비교해줍니다.
문자열 출력이지만 전형적인 형태의 문제로 yes/no 문제가 있는데, 이런 문제들만을 위한 nyesno.cpp
도 준비되어 있습니다. 특징으로는 대소문자를 구분하지 않으며, “yes”나 “no” 이외의 문자열이 입력된 경우에 대한 메시지를 따로 출력해준다는 점도 있습니다.
스페셜 저지
하지만 이렇게 기본으로 제공되는 예제 외에 특수한 판정 기준이 필요한 문제들도 있습니다. 답이 여럿이 될 수 있는 경우4를 위한 checker를 스페셜 저지라고 부릅니다. 이러한 경우에는 다음과 같은 절차를 거쳐 정답 여부를 판별하게 됩니다.
- 정답 코드에 의해 생성된 파일 읽기
- 참가자의 코드에 의해 생성된 출력 파일을 읽기
- 각각의 유효성(또는 참가자의 출력만)을 검사
- (필요하면) 두 코드의 답을 비교 (예: 가장 큰 답을 찾아야 하는 경우)
스페셜 저지를 구현한 예시로는 같은 대회의 D번 문제인 Checkpoints의 checker를 사용하도록 하겠습니다.
#include "testlib.h"
using namespace std;
using ll = long long;
const int MAX_T = 50;
const int MAX_N = 2000;
ll k;
void check(InStream &in)
{
int i;
ll ans = 0;
int n = in.readInt(-1, MAX_N);
if (n == -1)
{
if (k % 2 == 0)
in.quitf(_wa, "an answer exists but the participant couldn't find it");
return;
}
if (n == 0)
in.quitf(_wa, "0 stages is not allowed");
if (in.readInt(0, 1) != 1)
in.quitf(_wa, "there must be a checkpoint on the first stage");
int prev = 1;
for (i = 2; i <= n + 1; i++)
{
int x = (i != n + 1 ? in.readInt(0, 1) : 1);
if (x == 1)
{
int d = i - prev;
if (d >= 59 || (ans += (1ll << (d + 1)) - 2) > k)
in.quitf(_wa, "the expected number of tries exceeds k");
prev = i;
}
}
if (ans < k)
in.quitf(_wa, "the expected number of tries is less than k");
}
int main(int argc, char *argv[])
{
registerTestlibCmd(argc, argv);
int t = inf.readInt();
for (int tc = 1; tc <= t; tc++)
{
setTestCase(tc);
k = inf.readLong();
check(ans);
check(ouf);
}
int extraInOufCount = 0;
while (!ouf.seekEof())
{
ouf.readToken();
extraInOufCount++;
}
if (extraInOufCount > 0)
quitf(_wa, "Output contains extra information (%d tokens)", extraInOufCount);
quitf(_ok, "ok");
}
Checker의 등록: registerTestlibCmd
Checker 역시 generator, validator와 마찬가지로 testlib에 등록해야 하며, 이때는 registerTestlibCmd
함수가 사용됩니다.
registerTestlibCmd(argc, argv);
세 개의 파일: inf, ans, out
Checker는 무려 세 개의 파일을 읽어야 합니다. Validator에서 읽었던 것과 같은 입력 파일인 inf
, 정답 코드에 의해 만들어진 출력 파일인 ans
, 그리고 참가자의 코드에 의해 만들어진 출력 파일인 out
이 그것들입니다. 정답을 비교하는데 입력 파일이 필요한 이유는 정답의 유효성을 검사하기 위해서는 원래의 입력이 무엇이었는지 역시 checker가 알고 있어야 하기 때문입니다.
정답 코드와 참가자의 코드에 의해 만들어진 출력 파일인 ans
와 out
은 똑같은 방법으로 읽고 유효성 검사를 할 것이므로, 이 코드와 같이 하나의 유효성 검사 루틴을 두고 ans
와 out
각각에 대해 한 번씩 실행하는 것이 권장됩니다.
for (int tc = 1; tc <= t; tc++)
{
setTestCase(tc);
k = inf.readLong();
check(ans);
check(ouf);
}
이 부분에 사용된 setTestCase
함수는 다중 테스트 케이스 문제에서 틀렸을 때 testlib가 채점 시스템에 몇 번째 테스트 케이스에서 판정된 결과인지를 같이 전달하도록 해주는 간편한 함수입니다.
판정 결과 전달: quitf
check
함수에서는 주어진 출력 파일에 대한 유효성 검사를 합니다. 그런데 만일 중도에 유효하지 않은 부분을 발견했다면 그 자리에서 즉시 틀렸다는 판정을 내리고 종료해도 될 것입니다. 이 경우 quitf
함수를 사용하게 됩니다.
예시로 이 문제에서는 ‘try’의 기댓값이 정확히 $k$가 되어야 하므로, 이 값이 이미 $k$를 초과해버렸다면 이후는 볼 것도 없이 답이 틀린 것이므로 바로 오답 판정을 내려도 됩니다. 아래 코드가 이를 판정하는 부분입니다.
if (d >= 59 || (ans += (1ll << (d + 1)) - 2) > k)
in.quitf(_wa, "the expected number of tries exceeds k");
전역으로 선언된 quitf
도 있으나 일반적으로는 파일 스트림 객체의 멤버 함수 quitf
를 사용하는 것이 권장됩니다. quitf
의 첫 번째 인자로는 판정 결과를 전달하는데, 밑줄로 시작하는 enum 값들이 미리 여럿 정의되어 있습니다. 일반적으로는 _wa
만을 사용하면 됩니다. Checker나 정답 코드에 문제가 있음을 명시적으로 알리기 위한 _fail
도 있지만 정답 코드의 유효성을 검사하는 부분에서는 사용할 필요는 없습니다. 파일 스트림 객체가 이미 그 파일이 어떤 코드로부터 나온 결과물인지를 알고 있기 때문에 _wa
를 전달하더라도 해당 파일이 정답 코드의 출력이라면 fail로 처리를 해주기 때문입니다. quitf
의 두 번째 인자로는 checker의 메시지를 적어주면 됩니다.
또한 출력 결과가 정답인 경우 main
함수가 종료되기 전에도 quitf
를 호출해주어야 하며, 이때에는 _ok
를 전달해야 합니다.
quitf(_ok, "ok");
불필요한 추가 출력 확인하기
모든 정답을 출력한 이후 불필요한 출력을 추가로 했을 경우 정답으로 인정하고 싶지 않다면 그를 확인하는 코드 또한 추가해야 합니다. 이런 경우 주로 아래와 같은 코드를 활용하게 됩니다.
int extraInOufCount = 0;
while (!ouf.seekEof())
{
ouf.readToken();
extraInOufCount++;
}
if (extraInOufCount > 0)
quitf(_wa, "Output contains extra information (%d tokens)", extraInOufCount);
여기에서 사용된 seekEof
함수는 당장 EOF를 읽지 않으면 에러로 판정하는 readEof
함수와는 달리 화이트스페이스 등을 무시하고 EOF가 바로 나오는지를 확인해줍니다. 즉, 정답에 해당하는 출력 이후 화이트스페이스가 아닌 문자가 하나라도 더 남아있다면 이 부분에서 extraInOufCount
가 0이 아니게 되므로 틀린 것으로 판정하도록 만든 것입니다.
기타 유용한 기능들
이외에도 testlib에는 유용한 기능들이 많이 있습니다. 대표적으로 rnd
를 사용하여 리스트를 섞어주는 자체적인 shuffle
함수가 있으며, 수를 보다 읽기 쉬운 형태의 문자열로 만들어주는 toHumanReadableString
함수나 영어식으로 서수 문자열을 만들어주는 englishEnding
함수 등 편리하게 활용할 수 있는 다양한 유틸리티들이 포함되어 있습니다. 좋은 기능들을 놓치고 싶지 않다면 testlib.h
를 직접 열고 전체를 정독해보는 것도 괜찮을 것입니다.
-
이것은 생각보다 매우 중요한 기능입니다. 같은 인자를 통해 generator를 실행했는데 그때마다 매번 다른 입력 파일이 만들어진다면 원하는 데이터가 ‘우연히’ 생성되었을 때 항상 그 결과물을 통째로 저장해두고 있어야 하는데, 이는 이미 데이터 세트가 완성된 후라면 상관이 없겠지만 작업하는 도중에는 관리하기가 까다로우며 한 번 잃어버린 경우 쉽게 재생성할 수도 없습니다. ↩
-
그렇다면 “이 횟수를 너무 크게 하면 generator의 실행이 너무 느려질 수도 있지 않을까?”라고 생각할 수 있지만 걱정하지 않아도 됩니다. testlib 자체적으로
random_t::lim
이라는 한계값을 가지고 있으며, 이를 넘는 횟수가 전달될 경우 직접 그 횟수만큼을 다 골라보는 것이 아닌 공식을 통해 $\mathcal{O}(1)$ 시간에 근사적으로 기대하는 분포가 나오도록 수를 찾아줍니다. ↩ -
입력에 문제가 있을 때 전달된 그 문자열에 의한 것임을 에러 메시지로 알려줍니다. ↩
-
실제로는 하나이지만 참가자를 속이고 여럿인 것처럼 위장할 수도 있습니다. 답이 하나밖에 존재할 수 없음을 알아내는 것도 문제의 일부인 경우가 해당됩니다. ↩