4つの数字を自由に並び替えたり、演算子を加えたり、括弧を加えたりして10を作る問題があります。助手席に座っている時に前の車のナンバーで遊ぶ方法の一つです。この問題の名前ですがテンパズルというのが通称らしいです。
テンパズル – Wikipedia
テンパズルを題にして PHP8.2 で追加された Randomizer の使用例を作りました。これが下記です。
Randomizer ですが、目玉機能であるインスタンス別のシード値の他にも使用感が従来の組み込み関数といささか異なります。例えば shuffleArray は配列に破壊的変更を加えません。単に$randomizer->shuffleArray($arr);としても元の変数の中にある配列は書き換わりません。返り値を扱う必要があります。他にも array_rand 相当のメソッドが必ず配列を返したりと微妙な差異がちらほらあります。また、乱数のエンジンを Randomizer で設定し、その乱数のエンジンに従ってランダムな色々なことをするという流れになり、これまで暗号学的にセキュアでなかったランダムしか扱えなかった機能にについても、暗号学的にセキュアなランダムを扱える様になりました。
PHP: Random\Engine\Secure – Manual
正直あえて従来の方法を捨てて Randomizer を使うほどのインパクトはありません。細かい何かや再現性が不要の場合、組み込み関数を実行するのみの方が書きやすくて便利な時もあります。とはいえ確実に従来の方法より多機能です。もし組み込み関数のみでランダムを扱っている際に詰まったら、それは Randomizer でできるのではないかと探してみるのも良さそうです。
<?php
/**
* 渡された入力の計算結果が $tgt ならば true、$tgt でないならば false を返す
* @param string $expression
* @param int|float $tgt
* @return bool
*/
function isCalculatedEqTgt(string $expression, int|float $tgt = 10): bool
{
try {
// 式を PHP のコードとして計算
// 自分で作ったコードの中で演算子と括弧と数値のみが渡されると決まり切ってるので、
// この様に計算。外部から入力される値があるならばバリデーションは必須。
eval('$res = ' . $expression . ';');
// 誤差対策にわずかな違いならば等しい値とする
return abs($res - $tgt) / $tgt < 1e-4;
} catch(DivisionByZeroError $e) {
// 0割りは計算できないので false
return false;
}
}
/**
* 当たられた入力と乱数のシードで計算結果が期待通りになるか挑戦する。
* 期待通りならば乱数で作られた計算式が、期待通りでなければ null が返る
* @param array $input
* @param int $seed
* @return array|null
*/
function main(array $input, int $seed): ?array
{
/** @var string[] $op 演算子群。この中からランダムに使う演算子を選ぶ */
$op = ['+', '-', '*', '/'];
// PHP8.2より加わった乱数生成期クラスのインスタンスを与えられたシードで用意
// ここでは メルセンヌツイスタ を乱数生成アルゴリズムに選ぶ。
// 他にも暗号学的に安全な乱数生成ができる\Random\Engine\Secure などがある。
// @see https://www.php.net/manual/ja/class.random-randomizer.php
// @see https://www.php.net/manual/ja/class.random-engine-mt19937.php
$randomizer = new \Random\Randomizer(new \Random\Engine\Mt19937($seed));
// 配列を無作為に並び替える。旧来のshuffle関数と異なり、元の配列を破壊しなくなった
$shuffled = $randomizer->shuffleArray($input);
// 括弧。式で括弧を使う際に括弧の始まりと終わりが壊れない様にする
$bracketStack = [];
// [数値、演算子、数値、演算子、……]という形になる式を表現するランダムな配列を作る。
$expression = [];
// getInt は random_int 的に整数をランダムに取得するメソッド。使用感はまったく同じ
// 最初の括弧があるかないかをランダムに決める
if($randomizer->getInt(0, 1)) {
$expression[] = '(';
$bracketStack[] = 1;
}
$expression[] = $shuffled[0];
foreach(array_slice($shuffled, 1) as $num) {
// pickArrayKeys は array_rand 的に配列のキーをランダムに取得するメソッド。
// array_rand と異なり、明示的に第二引数を指定する必要があり、必ず配列で返ってくる。
$expression[] = $op[$randomizer->pickArrayKeys($op, 1)[0]];
// 前述同様にランダムに括弧の有無を決める
if($randomizer->getInt(0, 1)) {
$expression[] = '(';
$bracketStack[] = 1;
}
$expression[] = $num;
// もし開いたままの括弧が一つでもあったら閉じ括弧の有無を決める
if(count($bracketStack) > 0 && $randomizer->getInt(0, 1)) {
$expression[] = ')';
array_pop($bracketStack);
}
}
// 余った括弧を全部閉じる
foreach($bracketStack as $_){
$expression[] = ')';
}
// 作った式の結果が10になるか否かチェック。計算結果が 10 なら 10 になる式を返す
if(isCalculatedEqTgt(implode('', $expression), 10)) {
return $expression;
}
// 計算結果が 10 でないなら null を返す
return null;
}
// 0000 ~ 9999 の全てについて10を作れるか探索する
for($i = 0; $i < 10000; ++$i) {
/** @var array $input 0000 ~ 9999 の文字列を1文字ずつ分解した配列になる */
$input = str_split(str_pad((string)$i, 4, '0', STR_PAD_LEFT));
// 100000回までランダムな順と演算子で計算する
for($c = 0; $c < 10_0000; ++$c) {
$expression = main(input: $input, seed: $c);
if($expression !== null) {
break; // 正解が出れば終わり
}
}
// 結果の出力
echo implode(' ', $input) . "\t";
echo $c . "\t";
if(!isset($expression)) {
// 10が見つからなかった場合は null になるので文字列の null を出力
echo 'null';
} else {
// 10が見つかった場合は $answer に入っている計算式を出力
echo implode(' ', $expression);
}
echo "\n";
}