PHP で next permutation を作る

作成日:
石川県加賀市にある片山津温泉の総湯
石川県加賀市にある片山津温泉の総湯

Permutation (順列)とは

グレッグ・イーガンという作家の作品に、 ‘Permutation City’(順列都市)という難しい SF 小説がありますが、順列自体は、難しい考え方ではありません。

小・中学校の算数や数学で、「場合の数」と呼ばれていたものの1つで、たとえば、以下のような問題を解いたことがあるのではないでしょうか?

Aさん、Bさん、Cさんの3人が、一列に並びます。
何種類の並び順がありますか?

- A:B:C
- A:C:B
- B:A:C
- B:C:A
- C:A:B
- C:B:A
よって 6種類

公式を覚えている方の場合、 3! で6種類とすぐに求まったかもしれません。

順列とは、区別できるものを並べ替えて、その並び順を数え上げる時などに使われる概念です。

Next Permutation とは

C++ という言語に、 std::next_permutation() というメソッドがあります。
順列の組み合わせを、辞書順(アルファベット順)に並べたとき、ある順列の次の順列を高速に求めることができるメソッドです。

今回は、これと似たような関数を PHP で実装してみます。

PHP での実装

<?php
function next_permutation(array $arr, int $n): Generator {
    while (true) {
        yield $arr;
        $target = -1;
        for ($i = $n - 2; $i >= 0; $i--) {
            if ($arr[$i] < $arr[$i + 1]) {
                $target = $i;
                break;
            }
        }
        if ($target == -1) {
            return;
        }

        for ($i = 0; $target + 1 + $i < $n - 1 - $i; $i++) {
            $temp = $arr[$target + 1 + $i];
            $arr[$target + 1 + $i] = $arr[$n - 1 - $i];
            $arr[$n - 1 - $i] = $temp;
        }
        for ($j = $target + 1; $j < $n; $j++) {
            if ($arr[$j] > $arr[$target]) {
                $temp = $arr[$j];
                $arr[$j] = $arr[$target];
                $arr[$target] = $temp;
                break;
            }
        }
    }
}

// 使い方
$arr = [1, 2, 3, 4, 5, 6, 7, 8];
$n = 8;
$ans = 0;
foreach (next_permutation($arr, $n) as $perm) {
    $ans += 1;
}
var_dump($ans === (int) gmp_fact(8));

実装上のポイント

ジェネレータを使う

まず、すべて区別される順列の組み合わせ数は、 N! となり、数字が大きくなれば、爆発的に大きな数になってしまいます。

よって、関数はジェネレータとして作成し、次の順列が判明するごとに yield で返すことで、メモリを節約できます。

次の順列を見つけ出すロジック

手順は、大きく3つに分かれます。

  1. 後ろから順に見ていき、降順になっていないものを $target として取り出す
  2. $target 以下の降順部分を逆順にひっくり返す
  3. $target と、それより大きい最初の数を入れ替える

このロジックは、私が思いついたのではなく、ネット上の解説から作成しました。
どうしてこの手順で可能なのかは、私にもうまく証明できません。

しかし、実際にメモなどに書きつけて考えてみると、確かにうまくいくことが分かります。

1, 3, 5, 4, 2
     | <= target は、3
1, 3, 2, 4, 5  // 3 より後をひっくり返す
1, 4, 2, 3, 5  // 3 より大きい4と入れ替える

まとめ

もちろん、 C++ の std::next_permutation に処理速度で勝てるわけは無いのですが、とはいえ、 PHP で必要な場面では、それほど問題なく使えます。

もしさらに高速化したい場合は、関数内部で、下3桁か4桁程度を事前計算してしまい、問答無用で yield で返してしまうという技もあります。(ただし、これが通用するのは、要素すべてが区別できる場合です)

タグ一覧:

最後までお読みいただき、ありがとうございました。

何かお気づきの点がありましたら、 お問い合わせ ください。