Project Euler

Project Euler 問題3(最大の素因数)を解いてみる

Euler_003

Project Euler で紹介されている問題3を解いてみました。素因数分解の素朴なプログラミング例と、素因数の性質を使った改善案を紹介します。

問題3(最大の素因数)

13195 の素因数は5, 7, 13, 29 となる。

600851475143 の最大の素因数を求めよ。

解答例

与えられた数(600851475143)を2で割ります(実際には割れませんが)。
 もし2で割れたら最大の素因数(max_prime_factor)としてメモをする。
 さらに2で割り切れるだけ割り切る。

2で割り切れるだけ割った数に対して、次は3で割ります
 以下、同じ操作となります。

これを割り切れるだけ割った数(remain)まで繰り返す。

数が long int で表現できる数よりも大きいため、unsigned long long int を使います。型名が長いため、typedef することにします。

これを素直に実装しました。実装言語は C です。

#include <stdio.h>

typedef unsigned long long int ull;
#define TARGET_NO 600851475143

int main(void)
{
	ull i;
	ull target = TARGET_NO;
	ull remain = target;
	ull max_prime_factor = 1;

	for (i = 2; i <= remain; ++i) {
		if (remain % i == 0) {
			max_prime_factor = i;
			remain = remain / i;
			while (remain % i == 0) {
				remain = remain / i;
			}
		}
	}

	printf("Problem003: %lld\n", max_prime_factor);

	return 0;
}

小さい数から割れるだけ割っていきますので、結果的に割れる数は素数になります。例えば、先に2と3で割れるだけ割っているため、remain は素因数として2と3を含まず、当然6(= 2 × 3)では割れません。

考察

調べる数が固定されていては、アルゴリズムの性質がわかりにくいため、任意の数の素因数分解を行うプログラムに作り替えてみましょう。

#include <stdio.h>

typedef unsigned long long int ull;

int main(void)
{
	ull i;
	ull target;
	ull remain;

	scanf("%lld", &target);
	printf("%lld:", target);
	remain = target;

	for (i = 2; i <= remain; ++i) {
		while (remain % i == 0) {
			printf(" %lld", i);
			remain = remain / i;
		}
	}

	printf("\n");

	return 0;
}

プログラムを動かすと入力待ちになります。整数を1つ入力すると、素因数をすべて出力して、プログラムを終了します。試しに問題にある
  600851475143
を入力すると、すべての素因数を出力します。次に
  600851475149
を入力してください。はい、戻ってきませんね(paiza.IO で試したら、Timeout しました)。これは、600851475149 が素数なので、大きな数まで割っていく必要があり、計算機はがんばっているのですが、計算が終了しなくなっています。合成数の場合でも
  600851475145 (= 5 × 120170295029)
は計算量が多く戻ってきません。これは片方の素因数が大きいためです。

工夫してみましょう。

$n$ が自然数の積 $a \times b$ と書けるなら、$a$ か $b$ のどちらかは、$\sqrt{n}$ 以下となります。ですから、割る数は、$\sqrt{n}$ まで調べればよいわけです。$\sqrt{n}$ までの数で割り切って残った数は素数となることも分かります。

素因数分解プログラムの改善版

この原理を用いた実装は以下です($\sqrt{n}$ を求める代わりにループカウンタを二乗しています)。

#include <stdio.h>

typedef unsigned long long int ull;

int main(void)
{
	ull i;
	ull target;
	ull remain;

	scanf("%lld", &target);
	printf("%lld:", target);
	remain = target;

	for (i = 2; i * i <= remain; ++i) {
		while (remain % i == 0) {
			printf(" %lld", i);
			remain = remain / i;
		}
	}

	if (remain != 1) {
		printf(" %lld", remain);
	}
	printf("\n");

	return 0;
}

プログラムとしては、4行(背景色を薄く変更しました)しか変更がありませんが、劇的に速くなっています。このバージョンは、600851475143, 600851475149, 600851475145 に対して待ち時間なく素因数を返してきます。このバージョンは、試した数程度の大きさであれば計算パワーが少ない環境でも動作しますので、気楽に試してみてください。

最後に

$n$ の素因数を求めるためには、$\sqrt{n}$ 以下まで調べればよいことを使って改善しました。簡単な事例ですが、素因数分解の奥深さの一端でも伝わればと思います。

(ユーザは意識しませんが)世界中で使われている暗号アルゴリズムRSAは、大きな素数を2つ掛け合わせた数から元の2つの素数を求めることは、計算量が大きくて難しい、という現象に基づいています。

引き続き、Project Euler の問題を紹介していきます。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA