From e240c6e1569e9a10bcf5033867c69a0438c75c12 Mon Sep 17 00:00:00 2001 From: Kimiyuki Onaka Date: Thu, 20 May 2021 16:13:01 +0900 Subject: [PATCH] AtCoder Heuristic Contest 002 --- .github/workflows/measure.yml | 34 ++++- .github/workflows/visualize.yml | 70 +++++++++ main.cpp | 244 ++++++++++++++++++++++++++++++++ scripts/measure.py | 25 +++- scripts/visualize.py | 122 ++++++++++++++++ 5 files changed, 487 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/visualize.yml create mode 100644 main.cpp create mode 100644 scripts/visualize.py diff --git a/.github/workflows/measure.yml b/.github/workflows/measure.yml index 15d9f40..d629087 100644 --- a/.github/workflows/measure.yml +++ b/.github/workflows/measure.yml @@ -18,13 +18,43 @@ jobs: with: python-version: 3.6 + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: ${{ runner.os }}-pip- + - name: Install dependencies run: pip3 install -r scripts/requirements.txt + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - uses: actions/cache@v2 + id: cache-tools + with: + path: tools/ + key: ${{ runner.os }}-tools- + - name: Prepare tools/ + if: steps.cache-tools.outputs.cache-hit != 'true' + run: | + wget https://img.atcoder.jp/ahc002/c993bb7f09d9f8857fc90951fc6af11d.zip + unzip c993bb7f09d9f8857fc90951fc6af11d.zip + + - name: Build the visualizer run: | - wget https://img.atcoder.jp/ahc001/ded8fd3366b4ff0b0d7d053f553cdb84.zip - unzip ded8fd3366b4ff0b0d7d053f553cdb84.zip + cargo build --manifest-path=tools/Cargo.toml --release - name: Compile the code run: | diff --git a/.github/workflows/visualize.yml b/.github/workflows/visualize.yml new file mode 100644 index 0000000..bedf6e7 --- /dev/null +++ b/.github/workflows/visualize.yml @@ -0,0 +1,70 @@ +name: visualize + +on: [push, pull_request] + +jobs: + visualize: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install ffmpeg + run: sudo apt-get install ffmpeg + + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.6 + + - uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: ${{ runner.os }}-pip- + + - name: Install dependencies + run: pip3 install -r scripts/requirements.txt + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - uses: actions/cache@v2 + id: cache-tools + with: + path: tools/ + key: ${{ runner.os }}-tools- + + - name: Prepare tools/ + if: steps.cache-tools.outputs.cache-hit != 'true' + run: | + wget https://img.atcoder.jp/ahc002/c993bb7f09d9f8857fc90951fc6af11d.zip + unzip c993bb7f09d9f8857fc90951fc6af11d.zip + + - name: Build the visualizer + run: | + cargo build --manifest-path=tools/Cargo.toml --release + + - name: Compile the code + run: | + g++ -std=c++17 -Wall -O2 -DDUMP_UPDATE -Iac-library main.cpp + + - name: visualize the solution + run: | + python3 scripts/visualize.py --jobs 2 + + - name: Archive the video + uses: actions/upload-artifact@v2 + with: + name: video + path: vis/out.mp4 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..055f652 --- /dev/null +++ b/main.cpp @@ -0,0 +1,244 @@ +#include +#define REP(i, n) for (int i = 0; (i) < (int)(n); ++ (i)) +#define REP3(i, m, n) for (int i = (m); (i) < (int)(n); ++ (i)) +#define REP_R(i, n) for (int i = (int)(n) - 1; (i) >= 0; -- (i)) +#define REP3R(i, m, n) for (int i = (int)(n) - 1; (i) >= (int)(m); -- (i)) +#define ALL(x) std::begin(x), std::end(x) +using namespace std; + +class xor_shift_128 { +public: + typedef uint32_t result_type; + xor_shift_128(uint32_t seed = 42) { + set_seed(seed); + } + void set_seed(uint32_t seed) { + a = seed = 1812433253u * (seed ^ (seed >> 30)); + b = seed = 1812433253u * (seed ^ (seed >> 30)) + 1; + c = seed = 1812433253u * (seed ^ (seed >> 30)) + 2; + d = seed = 1812433253u * (seed ^ (seed >> 30)) + 3; + } + uint32_t operator() () { + uint32_t t = (a ^ (a << 11)); + a = b; b = c; c = d; + return d = (d ^ (d >> 19)) ^ (t ^ (t >> 8)); + } + static constexpr uint32_t max() { return numeric_limits::max(); } + static constexpr uint32_t min() { return numeric_limits::min(); } +private: + uint32_t a, b, c, d; +}; + +constexpr int N = 50; + +constexpr array DIRS = {{0, 1, 2, 3}}; +constexpr array DIR_Y = {-1, 1, 0, 0}; +constexpr array DIR_X = {0, 0, 1, -1}; +inline bool is_on_tiles(int y, int x) { + return 0 <= y and y < N and 0 <= x and x < N; +} + +inline uint16_t pack_point(int y, int x) { + return (y << 8) + x; +} + +inline pair unpack_point(int packed) { + return {packed >> 8, packed & ((1 << 8) - 1)}; +} + +string convert_to_command_string(const vector& result) { + assert (not result.empty()); + string ans; + REP (i, (int)result.size() - 1) { + auto [ay, ax] = unpack_point(result[i]); + auto [by, bx] = unpack_point(result[i + 1]); + if (by == ay - 1 and bx == ax) { + ans.push_back('U'); + } else if (by == ay + 1 and bx == ax) { + ans.push_back('D'); + } else if (by == ay and bx == ax + 1) { + ans.push_back('R'); + } else if (by == ay and bx == ax - 1) { + ans.push_back('L'); + } else { + assert (false); + } + } + return ans; +} + +template +string solve(const int sy, const int sx, const array, N>& tile, const array, N>& point, RandomEngine& gen, chrono::high_resolution_clock::time_point clock_end) { + chrono::high_resolution_clock::time_point clock_begin = chrono::high_resolution_clock::now(); + + int M = 0; + REP (y, N) { + REP (x, N) { + M = max(M, tile[y][x] + 1); + } + } + + vector path_prev; + path_prev.push_back(pack_point(sy, sx)); + vector used_tile_prev(M, INT16_MAX); + used_tile_prev[tile[sy][sx]] = 0; + array, N> used_pos_prev = {}; + used_pos_prev[sy][sx] = true; + vector score_prev; + score_prev.push_back(0); + score_prev.push_back(point[sy][sx]); + + vector result = path_prev; + int highscore = score_prev.back(); +#ifdef DUMP_UPDATE + int highscore_index = 0; + cerr << "-----BEGIN-----" << endl; + cerr << convert_to_command_string(result) << endl; + cerr << "-----END-----" << endl; +#endif // PRINT_UPDATE + + // simulated annealing + int64_t iteration = 0; + double temperature = 1.0; + for (; ; ++ iteration) { + if (iteration % 64 == 0) { + chrono::high_resolution_clock::time_point clock_now = chrono::high_resolution_clock::now(); + temperature = static_cast((clock_end - clock_now).count()) / (clock_end - clock_begin).count(); + if (temperature <= 0.0) { + cerr << "done (iteration = " << iteration << ")" << endl; + break; + } + } + + int start = uniform_int_distribution(1, path_prev.size())(gen); + vector used_tile_next(M); + auto get_used_tile = [&](int y, int x) { + int i = tile[y][x]; + return used_tile_prev[i] < start or used_tile_next[i]; + }; + vector diff; + int score_next = score_prev[start]; + auto [y, x] = unpack_point(path_prev[start - 1]); + while (true) { + array dirs = {{0, 1, 2, 3}}; + shuffle(ALL(dirs), gen); + bool found = false; + for (int dir : dirs) { + int ny = y + DIR_Y[dir]; + int nx = x + DIR_X[dir]; + if (not is_on_tiles(ny, nx)) { + continue; + } + if (diff.empty() and start < path_prev.size() and path_prev[start] == pack_point(ny, nx)) { + continue; + } + if (not get_used_tile(ny, nx)) { + found = true; + diff.push_back(pack_point(ny, nx)); + y = ny; + x = nx; + used_tile_next[tile[y][x]] = true; + score_next += point[y][x]; + break; + } + } + if (not found) { + break; + } + if (used_pos_prev[y][x]) { + break; + } + } + if (diff.empty()) { + continue; + } + int tail_first = path_prev.size(); + int tail_last = path_prev.size(); + if (used_pos_prev[y][x]) { + tail_first = start; + while (tail_first < path_prev.size() and path_prev[tail_first] != pack_point(y, x)) { + ++ tail_first; + } + assert (tail_first < path_prev.size()); + ++ tail_first; + REP3 (i, tail_first, path_prev.size()) { + auto [y, x] = unpack_point(path_prev[i]); + if (get_used_tile(y, x)) { + tail_last = i; + break; + } + used_tile_next[tile[y][x]] = true; + } + } + score_next += score_prev[tail_last] - score_prev[tail_first]; + + int delta = score_next - score_prev.back(); + auto probability = [&]() { + constexpr long double boltzmann = 0.01; + return exp(boltzmann * delta / temperature); + }; + if (delta >= 0 or bernoulli_distribution(probability())(gen)) { + // accept + if (delta < 0) { +#ifdef PRINT_UPDATE + cerr << "decreasing move (delta = " << delta << ", iteration = " << iteration << ")" << endl; +#endif // PRINT_UPDATE + } + + diff.insert(diff.end(), path_prev.begin() + tail_first, path_prev.begin() + tail_last); + path_prev.resize(start); + path_prev.insert(path_prev.end(), ALL(diff)); + used_tile_prev.assign(M, INT16_MAX); + used_pos_prev = {}; + score_prev.clear(); + score_prev.push_back(0); + REP (i, path_prev.size()) { + auto [y, x] = unpack_point(path_prev[i]); + used_tile_prev[tile[y][x]] = i; + used_pos_prev[y][x] = true; + score_prev.push_back(score_prev.back() + point[y][x]); + } + + if (highscore < score_prev.back()) { + highscore = score_prev.back(); + result = path_prev; +#ifdef PRINT_UPDATE + cerr << "highscore = " << highscore << " (iteration = " << iteration << ")" << endl; +#endif // PRINT_UPDATE +#ifdef DUMP_UPDATE + cerr << "-----BEGIN-----" << endl; + cerr << convert_to_command_string(result) << endl; + cerr << "-----END-----" << endl; +#endif // PRINT_UPDATE + } + } + } + + string ans = convert_to_command_string(result); + cerr << "ans = " << ans << endl; + cerr << "score = " << highscore << endl; + return ans; +} + +int main() { + constexpr auto TIME_LIMIT = chrono::milliseconds(2000); + chrono::high_resolution_clock::time_point clock_begin = chrono::high_resolution_clock::now(); + xor_shift_128 gen(20210425); + + int sy, sx; cin >> sy >> sx; + array, N> tile; + REP (y, N) { + REP (x, N) { + cin >> tile[y][x]; + } + } + array, N> point; + REP (y, N) { + REP (x, N) { + cin >> point[y][x]; + } + } + string ans = solve(sy, sx, tile, point, gen, clock_begin + chrono::duration_cast(TIME_LIMIT * 0.95)); + cout << ans << endl; + return 0; +} diff --git a/scripts/measure.py b/scripts/measure.py index 8bbb445..f343870 100644 --- a/scripts/measure.py +++ b/scripts/measure.py @@ -1,5 +1,6 @@ import argparse import concurrent.futures +import math import os import pathlib import subprocess @@ -15,8 +16,7 @@ def gen(*, seeds: List[int]) -> None: with open('seeds.txt', 'w') as fh: for seed in seeds: print(seed, file=fh) - with open('seeds.txt') as fh: - subprocess.check_call(['cargo', 'run', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--bin', 'gen'], stdin=fh) + subprocess.check_call(['cargo', 'run', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--release', '--bin', 'gen', 'seeds.txt']) def run(*, command: str, input_path: pathlib.Path, output_path: pathlib.Path, seed: int) -> None: @@ -32,12 +32,14 @@ def run(*, command: str, input_path: pathlib.Path, output_path: pathlib.Path, se def vis(*, input_path: pathlib.Path, output_path: pathlib.Path, vis_path: pathlib.Path, seed: int) -> int: logger.info('running the visualizer for seed %d...', seed) try: - score_str = subprocess.check_output(['cargo', 'run', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--bin', 'vis', '--', str(input_path), str(output_path)]) + score_bytes = subprocess.check_output(['cargo', 'run', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--release', '--bin', 'vis', '--', str(input_path), str(output_path)]) except subprocess.SubprocessError: logger.exception('failed for seed = %d', seed) return 0 os.rename('out.svg', vis_path) - return int(score_str) + if not score_bytes.startswith(b'Score = '): + raise RuntimeError(score_bytes.decode()) + return int(score_bytes.split()[2]) def main() -> 'NoReturn': @@ -45,13 +47,17 @@ def main() -> 'NoReturn': parser.add_argument('-c', '--command', default='./a.out') parser.add_argument('-n', '--count', type=int, default=50) parser.add_argument('-j', '--jobs', type=int, default=2) + parser.add_argument('--same', action='store_true') parser.add_argument('--seed', type=int, default=0) args = parser.parse_args() basicConfig(level=DEBUG) # gen - seeds = [args.seed + i for i in range(args.count)] + if args.same: + seeds = [args.seed for i in range(args.count)] + else: + seeds = [args.seed + i for i in range(args.count)] gen(seeds=seeds) # run @@ -72,7 +78,14 @@ def main() -> 'NoReturn': score = vis(input_path=input_path, output_path=output_path, vis_path=vis_path, seed=seed) scores.append(score) logger.info('seed = {}: score = {}'.format(seed, score)) - logger.info('50 * average = %d', int(50 * sum(scores) / len(scores))) + average = sum(scores) / len(scores) + if args.same: + logger.info('average = %s', average) + logger.info('min = %s', min(scores)) + logger.info('max = %s', max(scores)) + logger.info('standard deviation = %s', math.sqrt(sum([(score - average)**2 for score in scores]) / len(scores))) + else: + logger.info('100 * average = %s', int(100 * average)) if min(scores) <= 0: sys.exit(1) diff --git a/scripts/visualize.py b/scripts/visualize.py new file mode 100644 index 0000000..f692e5e --- /dev/null +++ b/scripts/visualize.py @@ -0,0 +1,122 @@ +import argparse +import concurrent.futures +import math +import os +import pathlib +import shutil +import subprocess +import sys +import tempfile +from logging import DEBUG, basicConfig, getLogger +from typing import * +from typing import BinaryIO + +logger = getLogger(__name__) + +BEGIN_MARKER = b'-----BEGIN-----' +END_MARKER = b'-----END-----' + + +def gen(*, seed: int) -> None: + logger.info('running the generator...') + with open('seeds.txt', 'w') as fh: + print(seed, file=fh) + command = ['cargo', 'run', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--release', '--bin', 'gen', 'seeds.txt'] + subprocess.check_call(command) + + +def parse_result(*, stdout: bytes, stderr: BinaryIO) -> List[bytes]: + outputs: List[bytes] = [] + lines = stderr.readlines() + l = 0 + while l < len(lines): + if lines[l].rstrip() == BEGIN_MARKER: + r = l + 1 + while r < len(lines) and lines[r].rstrip() != END_MARKER: + r += 1 + if r == len(lines): + raise RuntimeError('{} is not found'.format(repr(END_MARKER.decode()))) + outputs.append(b''.join(lines[l + 1:r])) + l = r + 1 + elif lines[l].rstrip() == END_MARKER: + raise RuntimeError('unexpected {} found'.format(repr(END_MARKER.decode()))) + else: + sys.stderr.buffer.write(lines[l]) + l += 1 + outputs.append(stdout) + return outputs + + +def vis(*, input_path: pathlib.Path, output_path: pathlib.Path, vis_path: pathlib.Path, index: int) -> int: + logger.info('running the visualizer for %d-th state...', index) + with tempfile.TemporaryDirectory() as tempdir_: + tempdir = pathlib.Path(tempdir_) + try: + command = [str((pathlib.Path.cwd() / 'tools' / 'target' / 'release' / 'vis').resolve()), str(input_path.resolve()), str(output_path.resolve())] + score_bytes = subprocess.check_output(command, cwd=tempdir) + except subprocess.SubprocessError as e: + raise RuntimeError('failed for index = %d' % index) from e + os.rename(tempdir / 'out.svg', vis_path) + if not score_bytes.startswith(b'Score = '): + raise RuntimeError(score_bytes.decode()) + return int(score_bytes.split()[2]) + + +def main() -> 'NoReturn': + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--command', default='./a.out') + parser.add_argument('-j', '--jobs', type=int, default=2) + parser.add_argument('--seed', type=int, default=0) + args = parser.parse_args() + + basicConfig(level=DEBUG) + + # gen + input_path = pathlib.Path('in', '0000.txt') + gen(seed=args.seed) + + # run + logger.info('running the command...') + pathlib.Path('out').mkdir(exist_ok=True) + output_path = pathlib.Path('out', 'err.txt') + with open(input_path, 'rb') as fh1: + with open(output_path, 'wb') as fh2: + proc = subprocess.run(args.command, stdin=fh1, stdout=subprocess.PIPE, stderr=fh2, check=True) + logger.info('done') + with open(output_path, 'rb') as fh: + outputs = parse_result(stdout=proc.stdout, stderr=fh) + logger.info('%d states found', len(outputs)) + if len(outputs) > 4096: + logger.info('too many states') + sys.exit(1) + + # vis + if pathlib.Path('vis').exists(): + shutil.rmtree(pathlib.Path('vis')) + pathlib.Path('vis').mkdir() + logger.info('build the visualizer...') + command = ['cargo', 'build', '--manifest-path', str(pathlib.Path('tools', 'Cargo.toml')), '--release', '--bin', 'gen'] + subprocess.check_output(command) + score_futures: List[concurrent.futures.Future] = [] + with concurrent.futures.ProcessPoolExecutor(max_workers=args.jobs) as executor: + for i, output in enumerate(outputs): + output_path = pathlib.Path('out', '%04d.txt' % i) + vis_path = pathlib.Path('vis', '%04d.svg' % i) + with open(output_path, 'wb') as fh: + fh.write(output) + score_futures.append(executor.submit(vis, input_path=input_path, output_path=output_path, vis_path=vis_path, index=i)) + scores = [future.result() for future in score_futures] + for i, score in enumerate(scores): + logger.info('index = {}: score = {}'.format(i, score)) + + # generate video + command = ['ffmpeg', '-pattern_type', 'glob', '-i', str(pathlib.Path('vis', '*.svg')), '-framerate', '60', '-vcodec', 'libx264', '-pix_fmt', 'yuv420p', 'vis/out.mp4'] + subprocess.check_call(command) + + if min(scores) <= 0: + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main()