競技プログラミングAtCoderを快適に解くための環境構築をする

PhpStormでPHPUnitを実行している画面

tl;dt

  • プログラミングコンテストで時間オーバーとなり、解けそうな問題に回答ができなかった
  • コードを実行して結果を確認するプロセスを効率化すれば、ロジックの考察により多くの時間を割くことができる
  • そもそも競技プログラミングでは、所与の入出力を満たすロジックの記述に専念すれば良い
  • ロジックをデバッグするためなら、テストツール(PHPUnit)とデバッガ(Xdebug)を手軽に使える環境があれば良い

AtCoderのコンテストは時間との戦い

AtCoderが開催している競技プログラミングのコンテストに参加しています。

コンテスト本番と過去問での練習は、アルゴリズムを使って問題を解く点では同じです。

両者の違いは、コンテストでは制限時間があることです。

通常のコンテストでは問題は6問出題されるため、制限時間内に解けるだけ問題を解かなければなりません。

さらに、回答を提出するスピードが他の人より早いと、自分のレートが高くなります。

このため、自分の書いたロジックが正解なのか、間違っているのか、間違っているなら、間違っている箇所を特定するスピードが早ければ早いほど高レート獲得に有利です。

PHPUnitとXdebugを導入してデータの処理過程と結果を確認する

プログラミングコンテストの問題形式は、与えられた入力に対する出力が正しいことを確認するものです。

入力と出力をチェックするなら、テストを書いて実行すればいいのです。

また、出力が想定通りではない場合は、ロジックが間違っているということです。

この場合、コードの実行過程を素早くチェックすることができれば、デバッグは容易になります。

このため、PHPUnitとXdebugを使えば、正解にたどり着くために高速でフィードバックループを回すことができます。

そこで、Dockerを使ってこの環境を構築することにしました。

完成したDockerfileなどのコードはGitHubのリポジトリにアップしています。

PHPUnitの便利な使い方

ジェネレータ関数とデータプロバイダーで複数パターンの入出力をシンプルに記述する

PHPUnitには@dataProviderというアノテーションがあります。

データプロバイダに指定した関数の返り値を、@dataProvider メソッド名というアノテーションをつけたメソッドの引数として扱う機能です。

<?php
use PHPUnit\Framework\TestCase;

class SampleTest extends TestCase
{
    /**
     * @group 100A
     * @dataProvider DataA
     */
    public function testA($expected, $a, $b)
    {
        $result = $this->solveA($a, $b);
        $this->assertSame($expected, $result);
    }

    /**
     * @return Generator
     */
    public function DataA()
    {
        // yield "0" => ["出力", "入力1", "入力2"];
        yield "1" => [7, "at", "coder"];
        yield "2" => [11, "php", "language"];
    }

    /**
     * 提出するロジック
     */
    private function solveA($a, $b)
    {
        return strlen($a . $b);
    }
}

このコードでは、メソッドDataA()の返り値をテストメソッドtestA()の引数として扱っています。

データプロバイダには結果の値を$expectedとして記述しておきます。

こうすることで、データプロバイダDataA()に入出力値を記述し、テストメソッドtestA()にアサーションを記述し、solveAにはロジックを記述することができます。

テストにおける役割をメソッドごとに分離することが可能になります。

結果、ロジックのコードを競技プログラミングの回答として提出すればいいことになります。

(本来、ロジックはアプリケーションコードとして記述するものですが、簡便のためテストクラスにプライベートメソッドして記述しています。

テストクラスの中にロジックが入っていることに違和感がある方は、/srcディレクトリを作って/src配下のクラスでロジック記述し、TDDで開発することも可能です)

groupアノテーションで実行したいテストを指定する

今回作成した環境では、PHPUnitは下記のコマンドで実行することができます。

$ docker run --rm -v $(pwd)/tests:/home/tests atcoder/php

PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 152 ms, Memory: 4.00MB

OK (3 tests, 3 assertions)

これは、DockerfileにENTRYPOINT ["vendor/bin/phpunit", "tests"]と記述しているため、コンテナを実行するとPHPUnitを実行する仕組みになっているためです。

上記のサンプルのテストケースでは、メソッドtestA()@groupアノテーションを付与しています。

/**
 * @group 100A
 * @dataProvider DataA
 */
public function testA($expected, $a, $b){...}

このため、dockerの実行コマンドに--group=100Aを加えて $ docker run --rm -v $(pwd)/tests:/home/tests atcoder/php --group=100Aとすれば、testAメソッドのみを指定して実行することができます。

$ docker run --rm -v $(pwd)/tests:/home/tests atcoder/php --group=100A
PHPUnit 6.5.14 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 94 ms, Memory: 4.00MB

OK (2 tests, 2 assertions)

競技プログラミングのコンテストでは、問題ごとに回答を提出するため、@groupアノテーションを使ってテストメソッドを指定することで、自分が今解いている問題のロジックをテストすることに集中することができます。

ジェネレータ関数でイテレータを実装する

データプロバイダにジェネレータ関数を利用しています。

/**
 * @return Generator
 */
public function DataA()
{
    // yield "0" => ["出力", "入力1", "入力2"];
    yield "1" => [7, "at", "coder"];
    yield "2" => [11, "php", "language"];
}

ジェネレータを利用することで、複数の入出力のパターンを記述することができます。

AtCoderでは入力・出力のサンプルとして2~3パターンが提示されるため、サンプルの数だけyieldでイテレーションのアイテムを記述しておけば、ロジックの実装に集中することができます。

PhpStormのRemote Debugを設定する

Dockerfileとphp.iniの設定はこちらを、PhpStormの設定はこちらを参考にしました。

AtCoderのPHP環境を構築するための注意事項

AtCoderのPHP7系のバージョンは7.0.15のみです。

これに対応するため、Dockerのコンテナイメージとしてphp:7.0.15-alpineを利用しています。

しかし、このバージョンは現在サポート対象外です。また、PHP7.0に対応しているPHPUnit6系も現在では公式サポート外です。

このため、今回GitHubで公開しているDockerfileは決して本番環境に使わないようにお願いします。

GitHubレポジトリを公開しています

上記で紹介した環境は、GitHubのリポジトリにまとめています。

READMEを読みながら、ぜひ使ってみてくださいね。

Happy Coding 🎉