数学をプログラミング

完全数を見つけるプログラムをもっと速く!メルセンヌ素数で改善してみよう

前回の記事「完全数をPythonで見つけよう!数論の不思議をプログラムで体感!」では、完全数の定義に従って、PythonとRustを使ってコードを実装し、1〜10000の範囲で完全数を探しました。

しかし、定義に忠実に約数をすべてリストアップしていく方法では、処理効率が非常に悪くなってしまいます。
そこで今回は、「完全数入門! 数の面白シリーズ!」で紹介した完全数の性質(メルセンヌ素数との関係)を活用して、より効率的に完全数を求める方法を試してみたいと思います。

完全数と完全数の性質(復習)

まず、完全数の定義を思い出します。

完全数 (Perfect Number)

自身以外の正の約数の和が、ちょうど自分自身に等しい数のこと

完全数の例として

  • 6 = (1 + 2 + 3)
  • 28 = (1 + 2 + 4 + 7 + 14)
  • 496
  • 8128[katex]</li> <!-- /wp:list-item --></ul> <!-- /wp:list --> <!-- wp:paragraph --> <p>などがあります。 <strong>メルセンヌ<strong>素数</strong></strong>([katex]2^n-1で表される素数)と2のべき乗の積は完全数になります。

    命題1

    2^n-1が素数ならば、2^{n-1}(2^n-1)は完全数である。

    この事実を使って完全数を探していきます。

    メルセンヌ数を使って完全数を探そう!

    まずはPythonで実装します。

    # 素数判定を行う関数
    def is_prime(n):
        # 2未満の数は素数ではない
        if n < 2:
            return False
        # 2から√nまでの数で割り切れるか調べる
        for i in range(2,int(n**0.5) + 1):
            if n % i == 0:
                # 割り切れたら素数ではない
                return False
        # 割り切れる数がなければ素数
        return True
    
    # 偶数完全数を生成する関数
    def generate_even_perfect_numbers(n):
        perfect_numbers = []  # 完全数を格納するリスト
        p = 2  # 素数指数の初期値 
        while True:
            # メルセンヌ数を計算
            mersenne_number = 2**p - 1
            # メルセンヌ素数かどうか調べる
            if is_prime(mersenne_number):
                # メルセンヌ素数であれば完全数を算出
                perfect_number = 2**(p-1) * mersenne_number
                # 調べる数の最大数まで調べたら終了
                if perfect_number > n:
                    break
                # そうでなければ作成した完全数をリストに追加
                perfect_numbers.append(perfect_number)
            p += 1
        return perfect_numbers
    
    if __name__ == '__main__':
        # 10000以下の偶数完全数を表示
        for pn in generate_even_perfect_numbers(10000):
            print(f"{pn}は完全数です")

    プログラムでやっていることは、generate_even_perfect_numbers関数に与えられた数までメルセンヌ数を計算して、その数が素数であれば2^{n-1}(2^n-1)を計算してリストに追加して、最後にそのリストの数を表示するということです。

    実行したら分かるのですが、前回のプログラムに比べてかなり処理速度が向上しています。
    試しにgenerate_even_perfect_numbers関数に与える数を1000000000000000にしてみましたが、かかった時間は0.003秒です!
    以前のプログラムで100000までの完全数を調べたら、176.91041秒かかりました。
    (1000000000000000で調べようと思いましたが、1時間以上まっても終わらなかったので断念しました。)

    (おまけ)Rustでもメルセンヌ数を使って完全数を探そう!

    上で作成したコードのRust版を作成します。

    use std::time::Instant; // 時間計測のためのモジュールをインポート
    
    // 素数判定を行う関数
    fn is_prime(n: u128) -> bool {
        // 2未満の数は素数ではない
        if n < 2 {
            return false;
        }
        // 2から√nまでの数で割り切れるか調べる
        let sqrt_n = (n as f64).sqrt() as u128;
        for i in 2..sqrt_n {
            if n % i == 0 {
                // 割り切れたら素数ではない
                return false;
            }
        }
        // 割り切れる数がなければ素数
        true
    }
    
    // 偶数完全数を生成する関数
    fn generate_even_perfect_numbers(n: u128) -> Vec<u128> {
        let mut perfect_numbers = Vec::new(); //完全数を格納するベクタ
        let mut p = 2;
    
        loop {
            //メルセンヌ数を計算
            let mersenne_number = 2u128.pow(p) - 1;
            // ルセンヌ素数かどうか調べる
            if is_prime(mersenne_number) {
                // メルセンヌ素数であれば完全数を算出
                let perfect_number = 2u128.pow(p - 1) * mersenne_number;
                // 調べる数の最大数まで調べたら終了
                if perfect_number > n {
                    break;
                }
                // そうでなければ作成した完全数をリストに追加
                perfect_numbers.push(perfect_number);
            }
            p += 1;
        }
        perfect_numbers
    }
    
    fn main() {
        // 開始時間の記録
        let start = Instant::now();
        for pn in generate_even_perfect_numbers(1000000000000000000000000000000000000) {
            println!("{} は完全数です", pn);
        }
        // 経過時間を計算
        let duration = start.elapsed();
        println!("かかった時間: {:.6}秒", duration.as_secs_f64());
    }

    上のプログラムを実行すると、

    6 は完全数です
    28 は完全数です
    120 は完全数です
    496 は完全数です
    8128 は完全数です
    33550336 は完全数です
    8589869056 は完全数です
    137438691328 は完全数です
    かかった時間: 0.001483秒

    となりました。
    PythonよりもRustが処理速度が速いので、このような結果になっています。
    試しに、1000000000000000000000000000000000000をgenerate_even_perfect_numbersの引数に与えてみたのですが、以下のようになりました。

    6 は完全数です
    28 は完全数です
    120 は完全数です
    496 は完全数です
    8128 は完全数です
    33550336 は完全数です
    8589869056 は完全数です
    137438691328 は完全数です
    2305843008139952128 は完全数です
    かかった時間: 21.303184秒

    かなり大きい数で調べてみたのですが、20秒程度でこの数までの完全数を探すことができています。

    最後に

    メルセンヌ素数を活用することで、完全数の探索をより効率的に行えることがわかりました。
    これまでに、

    という3本の記事で、完全数の性質や、それを探すプログラムの実装についてご紹介してきました。

    次回からは、「友愛数」という興味深い数の世界を、同じようにプログラムを通して探っていきたいと思います。お楽しみに!

    ABOUT ME
    MSK
    数学・プログラミング・ゲーム制作が大好きな30代エンジニア。 趣味でUnityやC言語を使って数学を可視化したり、小さなアプリを作ったりしています。 教育・学びの楽しさにも関心があります。