こんにちは、akamahです。ひょんなご縁からPHPerKaigi 2026に参加し、 そこで開催されたPHPerコードバトルというショートコーディングの技能を試す大会に出場しました。 この記事では、私が提出した回答の解説をしたいと思います。

なお、私が参加したのはオンライン予選ですが、大会終了後に全ての問題に挑戦できる機会があったため、 オフライン予選の問題についても回答を提出しています。 そのため、これらの回答は時間が限られたコンテスト中ではなく、十分に時間のある時に作成されたことをおことわりします。 (それから、私の提出時には他の方の回答が目に入る状況であったこともご承知ください)

ルール

PHPerコードバトルは次のようなルールで行われました。

  • スコアはコード中の全ASCII空白文字を除去した後のバイト数となる。
  • 先頭や末尾に置かれた <?php, <?, ?> はカウントされない。
  • ジャッジ環境のPHPのバージョンは8.5.3。
  • error_reportingE_ALL & ~E_WARNING & ~E_NOTICE & ~E_DEPRECATED で実行される。

オフライン予選:ラウンド1 - 回文

標準入力の各行に、英小文字からなる文字列が一つずつ並んでいます。
与えられた文字列が回文(前から読んでも後ろから読んでも同じになる文字列)であれば「Yes」、そうでなければ「No」を出力してください。
「racecar」なら「Yes」、「hello」なら「No」を出力します。
すべての行についてこの手順を繰り返してください。
https://t.nil.ninja/phperkaigi/2026/code-battle/golf/2/watch

回文かどうかを判定するというお題です。PHPには strrev() という文字列を反転させる関数があるためこれを使えば簡単です。
気をつけるべきこととしては、fgets() 関数の結果には改行が含まれるため、それを考慮する必要があります。
また、PHPでは改行文字を文字列に埋め込むことができ、ルールによってそれが除外できるため \n ではなく改行文字そのものを埋め込むのが効率的です。
また、末尾のセミコロン ;?> の直前であれば省略できると聞いたため、このテクニックも使用していきます。

<?php
while ($s = trim(fgets(STDIN)))
    echo $s == strrev($s) ? "Yes
" : "No
"
?>

これでスコアは57となります。……が、実はもう少し縮められます。 trim() によって改行文字を取り除くために6バイト消費しています。なんだか勿体無いですね。

問題を観察すると、入力に対して回文となるかどうかの判定を行えればよく、 仮に改行が残っていたとしても、対称性の判定さえできれば構わないことがわかります。

すなわち、入力が madam\n なら、 madam に対して strrev() を使って回文の判定をする代わりに \nmadam\n に対してその判定を行っても結果は変わりません。 そしてPHPの文字列埋め込みと改行文字がスコアにカウントされないことを利用すれば、$s の先頭に改行文字を付与し、このように書くことができます。

<?php
while ($s = fgets(STDIN))
    echo strrev("
$s") == "
$s" ? "Yes
" : "No
"
?>

これで trim() の使用を省略する代わりに " 記号を4つ使い、差し引き2バイトの節約、スコアは55となります。

オフライン予選:ラウンド2 - 素数

標準入力の各行に、1 以上 10000 以下の整数が一つずつ並んでいます。
与えられた整数が素数であれば「Yes」、そうでなければ「No」を出力してください。
「7」なら「Yes」、「9」なら「No」を出力します。
すべての行についてこの手順を繰り返してください。
https://t.nil.ninja/phperkaigi/2026/code-battle/golf/3/watch

最終的なコードはこうなり、スコアは74でした。

<?php
for (;$n=fgets(STDIN);print $s?"Yes
":"No
")
    for ($s = $i = $n - 1; $i-- > 2;)
        $s *= $n % $i > 0
?>

このコードでは愚直に素数判定を行なっています。$i$n - 1 から 2 までループさせて、 $n$i で割り切れるかを調べ、すべてのケースで割り切れないならば素数と判定しています。

内側のループの中に注目してもらうと、$n % $i > 0 なら割り切れないので true を、 そうでないなら割り切れてるので false を、 $s に対して掛けています。 すなわち、$s は今までに割り切れたかどうかを管理する変数で、一度でも割り切れたら0となります。

なお、PHPでは算術演算をする際には bool 型の falsetrue はそれぞれ 0 と 1 に変換されるため、ここでも型変換は不要となります。

さて、この内側のループはカウントアップではなく、カウントダウンとなっています。ここで重要なのが1の扱いです。

問題文では、入力は1から10,000までの自然数とされています。そのため、素数判定の例外ケースである1のパターンに対処する必要があります。

入力が1の場合は内側のループは1回も回りません。if文を使って特別扱いすることもできるのですが、$s = $n - 1 として $s をループ突入前に0にするのが最も手軽です。

さらに、$i2 から $n - 1 の間を動くため、$s の初期化を行うついでに $i$n - 1 にして、ループをカウントダウンにすることにより、 $s$i の初期化に使う式を使い回すことができます。

その後の判定結果の出力ですが、for ループの3番目の式の中で行っています。 ここは $i++ などを書くところにあたり、ループの本体が実行された後に無条件で実行される箇所です。

この中には任意の式をカンマ区切りで書くことができます。 すると、内側のforループに文を2つ書き {} で囲うのではなく、print を第3式に記述することで波括弧とセミコロンを節約できます。

なお、print は式ですが echo は文であるため、この場所には書けません。そのため泣く泣く1バイト長い print を使って出力を行っています。

余談ですが、実は入力として与えられる数が小さいうちは $s *= $n % $i > 0 ではなく $s *= $n % $i と書いてしまってもよいようです。 ジャッジ環境ではどうやら後者でも通るようでした。 (手元のテストケースで $n = 10000 だと通らなかったので比較演算を入れて提出しました)

オフライン予選:ラウンド3 - 頻度解析

標準入力の各行に、英小文字からなる文字列が一つずつ並んでいます。
文字列に含まれる各文字の出現回数を「文字:回数」の形式で、アルファベット順にカンマ区切りで出力してください。
「banana」なら「a:3,b:1,n:2」を出力します。
すべての行についてこの手順を繰り返してください。
https://t.nil.ninja/phperkaigi/2026/code-battle/golf/4/watch

この問題はPHPのあまり使われない関数 count_chars を使い、次のように書くことができました。スコアは96です。 

<?php
for (;$l = trim(fgets(STDIN)); $s[-1]="
")
    foreach (count_chars($l, 1) as $a => $c) 
        $s .= chr($a) . ":$c,";

echo $s
?>

今回の問題には count_chars() がバチバチにハマりました。 文字カウントができるだけでなく、返り値のキーがアルファベット順に並ぶことも功を奏しています。

さて、振り返ってみて、この問題の悩ましい部分は文字列をカンマ区切りにして出力することでした。 これに対処するためにはいくつかの選択肢がありました。

  • a:3 のような文字列を組み立てて配列に代入してから join するべきか?
  • それとも、カンマをつけるべきかどうかを内側で判定した方が安いか?
  • 出力は行ごとに行ってからバッファの変数 $s を初期化するか?

そのため、この問題にはいろいろと試行錯誤がありました。 最終的に、出力文字列を $s に組み立てていって、行の最後で最後のカンマを改行文字に置き換えるのがシンプルだという結論に達しました。

この方針によって、末尾の要素かどうかの判定を綺麗に省略することができました。

なお、今回 trim() を使っている理由ですが、改行文字を内側のループで排除するよりも、 最初から trim() を利用した方が全体的に短くなるため、今回は素直に採用しました。

あと、その他の技法をお話ししておきます。

コード中で $s が未定義変数のまま .= で文字列連結されていますが、PHP的には警告が出るくらいで実行は中断されません。 PHPerコードバトルのレギュレーションでは警告は抑制されているため、問題なく動作します。

また、$s[-1] のように負のインデックスを指定することで、文字列の後ろからアクセスすることができます。 (ちなみに、配列に対しては負のインデックスは特別な意味を持ちません。)

それと、出題者の nsfisis さんは参照を使った極めてエレガントな解法を作られているため、是非問題のページから見ていただきたいです。 参照を使ったPHPコードを美しいと思ったのは後にも先にもこの時だけでしょう。

謝辞

さて、この記事の終わりになりましたが、PHPerKaigi 2026の運営を行ってくださった皆様、会場や二次会で私と関わってくださった皆様、どうもありがとうございました! そして作問とジャッジシステムの運営をしてくださったnsfisisさんには深くお礼を申し上げたいです。 おかげさまでとても楽しく、刺激に満ち溢れた時間を参加者の方々と過ごすことができました。

次回はオフライン予選のエキシビジョン問題2つについてを、早めに書いてアップする予定です。

次回: PHPerKaigi 2026 PHPerコードバトル writeup #2