diff --git a/algorithmic/problems/107/interactor.cpp b/algorithmic/problems/107/interactor.cpp index b180e6c18..7989d910c 100644 --- a/algorithmic/problems/107/interactor.cpp +++ b/algorithmic/problems/107/interactor.cpp @@ -43,12 +43,13 @@ int main(int argc, char* argv[]) { if (op == 0) { i64 y = ouf.readLong(); if (y < 1 || y > 1000000000000000000) { - quitf(_wa, "Invalid query: element %d is out of range [1, 1000000000000000000]", y); + quitf(_wa, "Invalid query: element %lld is out of range [1, 1000000000000000000]", y); } println(std::gcd(x, y)); } else if (op == 1) { - int a = ans.readInt(), d = ouf.readInt(); + // The guess may be any int, so 2 * d and a - d need 64 bits. + i64 a = ans.readInt(), d = ouf.readInt(); if (std::abs(a - d) <= 7 || (2 * a >= d && 2 * d >= a)) { max = std::max(max, q); break; diff --git a/algorithmic/problems/112/chk.cc b/algorithmic/problems/112/chk.cc index 71bf75d89..b16b8d079 100644 --- a/algorithmic/problems/112/chk.cc +++ b/algorithmic/problems/112/chk.cc @@ -59,6 +59,12 @@ int main(int argc, char* argv[]) { double claimed_min_dist = ouf.readDouble(); + // testlib reads an overflowing literal such as 1e400 as inf, and the + // relative comparison below would treat inf as equal to any distance. + if (!std::isfinite(claimed_min_dist)) { + quitf(_wa, "Minimum distance is not a finite number"); + } + if (claimed_min_dist < 0) { quitf(_wa, "Minimum distance cannot be negative: %.10f", claimed_min_dist); } diff --git a/algorithmic/problems/28/interactor.cc b/algorithmic/problems/28/interactor.cc index 07924e81d..055d1144b 100644 --- a/algorithmic/problems/28/interactor.cc +++ b/algorithmic/problems/28/interactor.cc @@ -24,6 +24,7 @@ static double referenceScore(double r) { #include #include #include +#include using namespace std; @@ -32,6 +33,7 @@ char current[15]; int num[30]; int cnt = 0; mapvis; +setdict; void dfs(int now, int start){ if(cnt >= 1000)return; @@ -69,7 +71,9 @@ int main(int argc, char *argv[]) { int t = inf.readInt(), n = inf.readInt(); for(int i = 0; i < 26; i++){ num[i] = inf.readInt(); + for(int j = 1; j <= num[i]; j++)dict.insert(s[i][j]); } + ensuref((int)dict.size() == n, "The word lists hold %d words, but N is %d", (int)dict.size(), n); cout<= 'a' && qs[0] <= 'z') ? num[start] : 0; int match_cnt = 0; string ret[1010]; - for(int j = 1; j <= num[start]; j++){ + for(int j = 1; j <= limit; j++){ int match = 1; for(int t = 0; t < qs.length(); t++){ if(s[start][j][t] != qs[t])match = 0; @@ -105,10 +111,10 @@ int main(int argc, char *argv[]) { cout.flush(); } else if(operation == "answer"){ + // n distinct dictionary words are the whole dictionary. for(int j = 1; j <= n; j++){ string answers = ouf.readWord(); - int start = answers[0]-'a'; - if(answers > s[start][num[start]])quitf(_wa, "Invalid answer"); + if(!dict.count(answers))quitf(_wa, "Invalid answer"); if(vis[answers])quitf(_wa, "Repeated answer"); vis[answers] = 1; } diff --git a/algorithmic/tests/test_problem107.py b/algorithmic/tests/test_problem107.py new file mode 100644 index 000000000..3b6d6631c --- /dev/null +++ b/algorithmic/tests/test_problem107.py @@ -0,0 +1,64 @@ +"""A guess must not pass the error check by overflowing 2 * d (#203).""" +import math +from pathlib import Path +import re +import subprocess +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / 'algorithmic/problems/107/testdata' + + +@pytest.fixture(scope='module') +def interactor(tmp_path_factory): + executable = tmp_path_factory.mktemp('interactor107') / 'interactor' + subprocess.run(['g++', '-std=gnu++17', '-O2', '-I' + str(ROOT / 'algorithmic/judge/include'), + str(ROOT / 'algorithmic/problems/107/interactor.cpp'), '-o', str(executable)], check=True) + return executable + + +def play(interactor, tmp_path, data, answer, contestant): + inf, transcript, ans = [tmp_path / name for name in ('in', 'transcript', 'ans')] + inf.write_text(data);ans.write_text(answer) + return subprocess.run([str(interactor), str(inf), str(transcript), str(ans)], + input=contestant, text=True, capture_output=True, timeout=5) + + +def ratio(result): + return float(re.search(r'Ratio: ([0-9.]+)', result.stderr).group(1)) + + +# 720720 has 240 divisors. In 32-bit arithmetic the first three guesses pass, +# because 2 * d wraps to a positive value. +@pytest.mark.parametrize('guess', [-1500000000, -1073741825, -2147483528, -2147483648, 2147483647]) +def test_overflowing_guess_is_wrong(interactor, tmp_path, guess): + result = play(interactor, tmp_path, '1\n720720\n', '240\n', '1 %d\n' % guess) + assert result.returncode == 7, result.stderr + assert 'Wrong guess' in result.stderr and ratio(result) == 0 + + +def test_issue_repro_on_testdata_scores_zero(interactor, tmp_path): + games = int((DATA / '1.in').read_text().split()[0]) + result = play(interactor, tmp_path, (DATA / '1.in').read_text(), (DATA / '1.ans').read_text(), + '1 -1500000000\n' * games) + assert result.returncode == 7, result.stderr + assert 'Wrong guess' in result.stderr and ratio(result) == 0 + + +# Accepted iff |guess - divisors| <= 7 or 1/2 <= guess/divisors <= 2. +@pytest.mark.parametrize('x,divisors,guess,correct', [ + (720720, 240, 120, True), (720720, 240, 119, False), (720720, 240, 480, True), (720720, 240, 481, False), + (1, 1, -6, True), (1, 1, -7, False), (1, 1, 8, True), (1, 1, 9, False), +]) +def test_error_bounds_are_unchanged(interactor, tmp_path, x, divisors, guess, correct): + result = play(interactor, tmp_path, '1\n%d\n' % x, '%d\n' % divisors, '1 %d\n' % guess) + assert result.returncode == 7, result.stderr + assert ('Wrong guess' not in result.stderr) == correct + assert ratio(result) == (1 if correct else 0) + + +def test_query_count_still_scores(interactor, tmp_path): + result = play(interactor, tmp_path, '1\n720720\n', '240\n', '0 16\n1 240\n') + assert result.returncode == 7, result.stderr + assert result.stdout.split() == ['1', '16'] + assert ratio(result) == pytest.approx((60 + math.log(40)) / 100, abs=1e-9) diff --git a/algorithmic/tests/test_problem112.py b/algorithmic/tests/test_problem112.py new file mode 100644 index 000000000..a7b997bb4 --- /dev/null +++ b/algorithmic/tests/test_problem112.py @@ -0,0 +1,66 @@ +"""A claimed distance must be finite: testlib reads 1e400 as inf (#202).""" +import math +from pathlib import Path +import re +import subprocess +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / 'algorithmic/problems/112/testdata' +N = int((DATA / '12.in').read_text()) + + +@pytest.fixture(scope='module') +def checker(tmp_path_factory): + executable = tmp_path_factory.mktemp('checker112') / 'checker' + subprocess.run(['g++', '-std=gnu++17', '-O2', '-I' + str(ROOT / 'algorithmic/judge/include'), + str(ROOT / 'algorithmic/problems/112/chk.cc'), '-o', str(executable)], check=True) + return executable + + +def spread(n): + # Fibonacci lattice on the unit sphere. + points = [] + for i in range(n): + z = 1 - (2 * i + 1) / n + theta = i * math.pi * (3 - math.sqrt(5)) + points.append((math.sqrt(1 - z * z) * math.cos(theta), math.sqrt(1 - z * z) * math.sin(theta), z)) + return points + + +def output(claim, points): + return claim + '\n' + ''.join('%.17g %.17g %.17g\n' % p for p in points) + + +def check(checker, tmp_path, text): + out = tmp_path / 'output' + out.write_text(text) + return subprocess.run([str(checker), str(DATA / '12.in'), str(out), str(DATA / '12.ans')], + text=True, capture_output=True, timeout=10) + + +@pytest.mark.parametrize('claim', ['1e400', '1e309']) +@pytest.mark.parametrize('placement', ['origin', 'spread']) +def test_infinite_claim_is_wrong(checker, tmp_path, claim, placement): + points = [(0.0, 0.0, 0.0)] * N if placement == 'origin' else spread(N) + result = check(checker, tmp_path, output(claim, points)) + assert result.returncode == 1, result.stderr + assert 'not a finite number' in result.stderr + + +def test_infinite_coordinate_is_wrong(checker, tmp_path): + # The sphere check rejects it, since inf * inf is inf; testlib never reads a NaN. + result = check(checker, tmp_path, '0\n1e400 0 0\n' + '0 0 0\n' * (N - 1)) + assert result.returncode == 1, result.stderr + assert 'outside the unit sphere' in result.stderr + + +def test_matching_claim_still_scores(checker, tmp_path): + points = spread(N) + claim = min(math.dist(p, q) for i, p in enumerate(points) for q in points[:i]) + result = check(checker, tmp_path, output(repr(claim), points)) + assert result.returncode == 7, result.stderr + ref = float((DATA / '12.ans').read_text().split()[0]) + lower = 60 * claim / ref + expected = lower if lower <= 60 else min(100, 60 + math.log(claim / ref)) + assert float(re.search(r'Ratio: ([0-9.]+)', result.stderr).group(1)) == pytest.approx(expected / 100, abs=1e-9) diff --git a/algorithmic/tests/test_problem28.py b/algorithmic/tests/test_problem28.py new file mode 100644 index 000000000..e8325517c --- /dev/null +++ b/algorithmic/tests/test_problem28.py @@ -0,0 +1,85 @@ +"""The answer must be the dictionary itself, not any n distinct words (#204).""" +import math +from pathlib import Path +import re +import subprocess +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / 'algorithmic/problems/28/testdata' + + +@pytest.fixture(scope='module') +def interactor(tmp_path_factory): + executable = tmp_path_factory.mktemp('interactor28') / 'interactor' + subprocess.run(['g++', '-std=gnu++17', '-O2', '-I' + str(ROOT / 'algorithmic/judge/include'), + str(ROOT / 'algorithmic/problems/28/interactor.cc'), '-o', str(executable)], check=True) + return executable + + +def words(letter, count): + # The interactor's list for a letter: the first `count` ten-letter words that start with it. + return [letter + 'aaaaaa' + ''.join(chr(97 + j // 26**k % 26) for k in (2, 1, 0)) for j in range(count)] + + +def play(interactor, tmp_path, data, contestant): + inf, transcript, ans = [tmp_path / name for name in ('in', 'transcript', 'ans')] + inf.write_text(data);ans.write_text('0\n') + return subprocess.run([str(interactor), str(inf), str(transcript), str(ans)], + input=contestant, text=True, capture_output=True, timeout=5) + + +def ratio(result): + return float(re.search(r'Ratio: ([0-9.]+)', result.stderr).group(1)) + + +# Two words start with 'a' and two with 'c'. +SMALL = '1\n4\n2 0 2' + ' 0' * 23 + '\n' +DICTIONARY = words('a', 2) + words('c', 2) + + +@pytest.mark.parametrize('answer', [ + ['a0', 'a1', 'a2', 'a3'], + DICTIONARY[:3] + ['c'], + DICTIONARY[:3] + ['Aaaaaaaaaa'], + DICTIONARY[:3] + ['baaaaaaaaa'], + DICTIONARY[:3] + ['aaaaaaaaac'], +]) +def test_word_outside_the_dictionary_is_wrong(interactor, tmp_path, answer): + result = play(interactor, tmp_path, SMALL, 'answer ' + ' '.join(answer) + '\n') + assert result.returncode == 1, result.stderr + assert 'Invalid answer' in result.stderr + + +def test_repeated_word_is_wrong(interactor, tmp_path): + answer = [DICTIONARY[0]] + DICTIONARY[:3] + result = play(interactor, tmp_path, SMALL, 'answer ' + ' '.join(answer) + '\n') + assert result.returncode == 1, result.stderr + assert 'Repeated answer' in result.stderr + + +# The first character used to index s[] and num[] unchecked; only '{' to '~' +# landed in the four spare rows. +@pytest.mark.parametrize('prefix', ['A', '0a', '\x7f', '{']) +def test_prefix_without_a_leading_letter_matches_nothing(interactor, tmp_path, prefix): + result = play(interactor, tmp_path, SMALL, 'query %s 1\nanswer %s\n' % (prefix, ' '.join(DICTIONARY))) + assert result.returncode == 7, result.stderr + assert result.stdout.split()[2:] == ['0'] + assert ratio(result) == pytest.approx((60 + math.log(2400)) / 100, abs=1e-9) + + +def test_dictionary_in_any_order_scores(interactor, tmp_path): + result = play(interactor, tmp_path, SMALL, 'query a 2\nquery c 2\nanswer ' + ' '.join(DICTIONARY[::-1]) + '\n') + assert result.returncode == 7, result.stderr + assert result.stdout.split()[2:] == ['2'] + DICTIONARY[:2] + ['2'] + DICTIONARY[2:] + assert ratio(result) == pytest.approx((60 + math.log(2400 / 4)) / 100, abs=1e-9) + + +@pytest.mark.parametrize('test', range(1, 11)) +def test_full_dictionary_of_each_test_is_accepted(interactor, tmp_path, test): + data = (DATA / ('%d.in' % test)).read_text() + counts = list(map(int, data.split()[2:28])) + answer = [w for i, count in enumerate(counts) for w in words(chr(97 + i), count)] + result = play(interactor, tmp_path, data, 'answer ' + ' '.join(answer) + '\n') + assert result.returncode == 7, result.stderr + assert ratio(result) == 1