PHPで完全数を求める

(1/1)
最初の10個の完全数
自分自身が自分自身を除く正の約数の和に等しくなる自然数のことを完全数(perfect number)と呼ぶ。たとえば 6 の約数は 1, 2, 3, 6 だが、自分自身を除いた和 1 + 2 + 3 は 6 に等しくなるので、6 は完全数である。
古代から 6, 28, 496, 8128 の4つが完全数であることが知られているが、ピタゴラスが名付けたとか、世界を創造した6日間と月の公転周期の28日が含まれているからキリスト教の神を完全性を表しているなどと言われてきた。
完全数を求める方程式は発見されていない。自然数を1つずつ虱潰しにしていかなければならないわけだが、こういうときに黙々と作業をするコンピュータが重宝する。
今回は、PHPで完全数を求めるプログラムを作るとともに、計算量について考える。

(2024年11月3日)全面改訂;GMP関数を導入

目次

サンプル・プログラムの実行例

PHPで完全数を求める

サンプル・プログラム

圧縮ファイルの内容
perfectNumbers.phpサンプル・プログラム本体。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「PHPでGET/POSTでフォームから値を受け取る」「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
perfectNumbers.php 更新履歴
バージョン 更新日 内容
2.0.0 2024/11/03 全面改訂;GMP関数を導入
1.1.0 2024/11/03 最大値を最大桁数に変更
1.0.0 2024/10/18 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
1.8.0 2024/11/12 validRegexPattern() 追加
1.7.0 2024/10/09 validURL() validEmail() 追加
1.6.0 2024/10/07 isButton() -- buttonタグに対応
1.5.0 2024/01/28 exitIfExceedVersion() 追加
1.4.2 2024/01/28 exitIfLessVersion() メッセージ修正

準備:初期値など

perfectNumbers.php

  56: // 初期値(START) =============================================================
  57: // 表示幅(ピクセル)
  58: define('WIDTH', 600);
  59: 
  60: // デフォルトの計算方式
  61: define('DEFAULT_METHOD', 2);
  62: 
  63: // デフォルトの最大桁数
  64: define('DEFAULT_MAX', 100);
  65: 
  66: // 最大桁数の最小値
  67: define('MIN_MIN', 2);
  68: 
  69: // 最大桁数の最大値
  70: define('MIN_MAX', 3000);
  71: 
  72: // 初期値(END) ===============================================================

各種定数は変更可能である。
データ入力に関わる関数群 "pahooInputData.php" はinclude_pathが通ったディレクトリに配置すること。

解説:総当たりで完全数を求める

この完全数の定義をそのままプログラムにしたものがユーザー定義関数 "getPerfectNumbersForce" である。

perfectNumbers.php

 148: /**
 149:  * 完全数かどうかを判定する(通常版)
 150:  * @param   int $n 判定する値
 151:  * @return  bool TRUE:完全数である / FALSE:完全数ではない
 152: */
 153: function isPerfectNumber($n) {
 154:     $sum = 0;
 155: 
 156:     // 自分自身以外の約数の和を求める
 157:     for ($i = 1$i < $n$i++) {
 158:         if ($n % $i == 0) {     // nがiで割り切れる場合
 159:             $sum +$i;
 160:         }
 161:     }
 162: 
 163:     // 和が元の数と等しければ完全数
 164:     return $sum === $n;
 165: }

perfectNumbers.php

 196: /**
 197:  * 完全数を求める(総当たり計算)
 198:  * @param   int $num 探索範囲の最大桁数
 199:  * @param   int $method 0:通常判定, 1:高速判定
 200:  * @return  array 完全数の配列
 201: */
 202: function getPerfectNumbersForce($num, $method) {
 203:     $maxInteger = pow(10, $num);    // 10^numを計算して、最大値を設定
 204:     $perfectNumbers = array();      // 完全数を格納する配列
 205: 
 206:     for ($i = 2$i <$maxInteger$i++) {
 207:         if ($method == 0) {
 208:             if (isPerfectNumber($i)) {
 209:                 $perfectNumbers[] = $i;
 210:             }
 211:         } else {
 212:             if (isPerfectNumberRapid($i)) {
 213:                 $perfectNumbers[] = $i;
 214:             }
 215:         }
 216:     }
 217:     return $perfectNumbers;
 218: }

探索範囲の最大桁数(10進数)を引数で指定し、その範囲にある完全数を配列に格納する。
自分自身以外に約数をもつ 2 からはじめて、指定した範囲まで、1つずつユーザー関数 isPerfectNumber に渡し、完全数の定義に従っているかどうかを判定する。
isPerfectNumber の中では、エラトステネスの篩のような高速化アルゴリズムは使わず、総当たりで約数を求める。

5桁までの探索をやらせると、けっこう時間がかかる。しかも悲しいことに、冒頭の4つの完全数しか見つからない。5番目の完全数は 33550336 であり、これに到達するまでには相当な時間がかかるだろう。2024年(令和6年)10月12日発見された52個目の完全数は4102万4320桁の数字で、51番目のものより1600万桁以上大きくなっている。

解説:完全数の判定を高速化する

総当たりで完全数を求めるとき、完全数の判定を行うユーザー定義関数 "isPerfectNumber" の計算量は \( O(max) \times O(n) \) である。計算時間を短縮するため、まず、この計算量を減らすことを目指す。
\( 1 \) と自分自身 \( n \) を除く約数の1つは、\( \sqrt{n} \) 以下であることは自明である。そして、その数とペアになる約数がある。

perfectNumbers.php

 167: /**
 168:  * 完全数かどうかを判定する(高速化)
 169:  * @param   int $n 判定する値
 170:  * @return  bool TRUE:完全数である / FALSE:完全数ではない
 171: */
 172: function isPerfectNumberRapid($n) {
 173:     // 自分以外の約数を持たない場合は棄却
 174:     if ($n < 2) {
 175:         return false;
 176:     }
 177: 
 178:     $sumDiv = 1;  // 1はどの数の約数にも含まれる
 179: 
 180:     // 平方根までの約数を探し、ペアの約数を加える
 181:     $sqrtNum = sqrt($n);
 182:     for ($i = 2$i <$sqrtNum$i++) {
 183:         if ($n % $i == 0) {
 184:             $sumDiv +$i;               // 約数を加算
 185:             $pairDiv = $n / $i;          // ペアの約数
 186:             if ($pairDiv !$i) {        // ペアの約数を加算
 187:                 $sumDiv +$pairDiv;
 188:             }
 189:         }
 190:     }
 191: 
 192:     // 和が元の数と等しければ完全数
 193:     return $sumDiv === $n;
 194: }

そこで、isPerfectNumberRapid では、まず \( \sqrt{n} \) を計算し、それ以下の範囲で1つずつ約数を探索し、同時にペアとなる約数を見つける。
こうすることで、計算量は多くても \( O(max) \times O(\sqrt{n}) \) に減少する。1~2桁の計算をしているときにはそれほど計算量は変わらないが、6桁(100万)になると、isPerfectNumber の計算量が \( O(1,000,000) \) であるのに比べ、isPerfectNumberRapid は \( O(1,000) \) と、3桁も減少する。つまり、桁数が増えれば増えるほど、計算速度の差が開く。

完全数を表示するユーザー関数 getPerfectNumbersForce の第2引数は省略可能だが、1を代入すると、isPerfectNumberRapid を使って判定する。

解説:ユークリッド・オイラーの定理

次に計算量が多いのが、ユーザー関数 getPerfectNumbersForce の中で、1つ1つの整数に対して完全数かどうかの判定を行っているループ処理――計算量で言えば \( O(max) \) の方だ。

じつは、偶数の完全数に限っては、18世紀の数学者レオンハルト・オイラーユークリッド・オイラーの定理を発見しており、これを使うと計算回数を劇的に少なくすることができる。
ユークリッド・オイラーの定理によると、
\[ P = 2^{p - 1} \times (2^p - 1) \]
\( p \) が素数であり、なおかつ \( 2^p - 1 \) も素数である場合(これをメルセンヌ素数と呼ぶ)、\( P \) は偶数の完全数になる。
そして、2024年(令和6年)10月に発見された52個目まで、たまたま、すべて偶数の完全数だった。奇数の完全数が存在するかどうかは証明されていないのだが、少なくともパソコンのパワーでで52個まで計算するのは不可能だから、ユークリッド・オイラーの定理 を使って高速化させてもらう。

perfectNumbers.php

 221: /**
 222:  * メルセンヌ素数かどうかを判定する.
 223:  * @param   unsigned int p 判定する指数
 224:  * @return  bool TRUE:メルセンヌ素数である / FALSE:メルセンヌ素数ではない
 225: */
 226: function isMersennePrime($p) {
 227:     // 2^p - 1を計算
 228:     $mersenne = gmp_sub(gmp_pow(2, $p), 1);
 229: 
 230:     // 確率的素数判定
 231:     return gmp_prob_prime($mersenne, 25> 0;
 232: }

perfectNumbers.php

 234: /**
 235:  * 完全数を生成する
 236:  * @param   int $p メルセンヌ指数
 237:  * @return  gmp 完全数
 238: */
 239: function generatePerfectNumber($p) {
 240:     // 完全数を計算: (2^(p-1)) * (2^p - 1)
 241:     $part1 = gmp_pow(2, $p - 1);
 242:     $part2 = gmp_sub(gmp_pow(2, $p), 1);
 243:     $perfect = gmp_mul($part1, $part2);
 244: 
 245:     return $perfect;
 246: }

perfectNumbers.php

 248: /**
 249:  * 完全数を求める(ユークリッド・オイラーの定理)
 250:  * @param   int $num 探索範囲の最大桁数
 251:  * @return  array 完全数の配列
 252: */
 253: function getPerfectNumbers($num) {
 254:     $maxInteger = gmp_pow(10, $num);    // 10^numを計算して、最大値を設定
 255:     $perfectNumbers = array();          // 完全数を格納する配列
 256: 
 257:     $index = 1;
 258:     $p = 2;
 259: 
 260:     while (TRUE) {
 261:         // メルセンヌ素数をチェック
 262:         if (isMersennePrime($p)) {
 263:             // 完全数を生成
 264:             $perfectNumber = generatePerfectNumber($p);
 265:             
 266:             // 最大値を超えたらループ終了
 267:             if (gmp_cmp($perfectNumber, $maxInteger> 0) {
 268:                 break;
 269:             }
 270: 
 271:             // 結果を出力
 272:             $perfectNumbers[] = gmp_strval($perfectNumber);
 273:             $index++;
 274:         }
 275:         $p++;
 276:     }
 277:     return $perfectNumbers;
 278: }

ユーザー関数 isMersennePrime を使ってメルセンヌ素数であれば、generatePerfectNumber を使って完全数を生成する。
この方法では、計算量は \( O(log_2 \ max) \) となる。2桁(100)の時でも計算量が6に減少するし、6桁(100万)の時には19に激減する。
したがって、総当たり計算と同じ時間ではるかに大きな桁数の完全数を求めることができるので、完全数の型を unsigned long ではなく、GMPの mpz_class に変更し、事実上、無限大の完全数を求められるようにした。現行のノートPCでも、3000桁程度までであれば、数分で出力するだろう。

解説:計算量は非機能要件として検討する

今回取り上げた完全数のように、計算量の多い問題では、ほとんどの場合、ループ処理が律速段階になっている。そこへ前提条件を導入することで計算量を劇的に減らせることができる場合がある。今回は、「偶数の完全数を求める」というのが前提条件だった。
結果を早く求めることができればユーザー喜ぶから、システムの非機能要件として計算量を減らすことを検討するといい。そして、前提条件を導入するかどうかはユーザーに諮らなければならないことだから、数学的に計算量 \( O \) を求め、どのくらい短縮できるかを数字をもって示すようにしよう。

参考サイト

(この項おわり)
header