私は、全ての数学の問題を机上のみで解決出来るとは考えていません。
手作業でやるには無理があるような問題、膨大な量のデータを探索したり出力したりする場合は、プログラミングで解決するということをやっている。
プログラミングを学ぶと、よく出てくるループ処理や再帰処理。
どっちが良いとか、悪いとか、可読性が高いとか、低いとか、いろいろあるだろう。
今回は互いに素について、プログラミングを使っていろいろやってみようかと思う。
互いに素とは、私のブログでは度々登場する言葉ですが、今一度説明します。
小学校の高学年になると分数を習うことだろう。
分数は分母分子が違っていても、値としては同じということがあります。
分数の答えを統一するために、数学でいうところのユニーク(一意)にするために、約分という計算をします。
これ以上約分出来ない分数を、数学では既約分数(きやくぶんすう)と言います。
互いに素とは、分母と分子がこれ以上約分出来ないことと同じです。
計算でいうならば、分母と分子の最大公約数が1だといううことと同じです。
2つの数に限らず、いくつかの数においても言えます。
あるn個の数が、互いに同じ約数を持たない。
あるn個の数の最大公約数が1である。
互いに素ということは、こういうことです。
数学の問題に、2つの自然数が互いに素である確率というものがあります。
先に答えを言ってしまうと、
6/π2≒0.60792710185402662866327677925836583342615264803347…
という値になります。
つまり、適当な分母分子が共に自然数の分数があると、6/π2の確率で既約分数であるということです。
では、C言語を使って適当にプログラミングしてみます。
coprime_loop.c
#include <stdio.h>
#include <stdlib.h>
unsigned int gcd(unsigned int i, unsigned int j)
{
unsigned int k;
while (j > 0) {
k = i%j;
i = j;
j = k;
}
return i;
}
int main(int argc, char *argv[])
{
unsigned int m, n, true, false, fin;
if ( argc != 2 ) return EXIT_SUCCESS;
fin = atoi(argv[1]);
true = false = 0;
for (m=2; m<fin; m++) {
for (n=1; n<m; n++) {
if ( gcd(m,n) == 1 ){
true++;
} else {
false++;
}
}
}
printf("true=%u, false=%u, ratio=%f\n", true, false, (double)true/(true+false));
return EXIT_SUCCESS;
}
ループ処理で求めていることがわかりますね。
プログラム名を coprime_loop とすると、
coprime_loop 100
のように、第一引数で fin の値を設定します。
サブルーチンの gcd は、ユークリッドの互除法という最大公約数を求めるのに使われる最も有効なアルゴリズムです。
プログラミング言語においても、既に確立されたものですので、誰が書いても同じ様な処理になるかと思います。
mとnの二重ループの終了判定から分かる通り、fin > m > n という関係を保ちます。
つまり、n/m という分数ならば、1未満の分数について調べているということになります。
fin | true | false | ratio |
4 | 3 | 0 | 1.000000 |
8 | 17 | 4 | 0.809524 |
16 | 71 | 34 | 0.676190 |
32 | 307 | 158 | 0.660215 |
64 | 1227 | 726 | 0.628264 |
128 | 4957 | 3044 | 0.619548 |
256 | 19819 | 12566 | 0.611981 |
512 | 79595 | 50710 | 0.610836 |
1024 | 318451 | 204302 | 0.609181 |
2048 | 1274561 | 819520 | 0.608649 |
4096 | 5097971 | 3284494 | 0.608116 |
8192 | 20397513 | 13144632 | 0.608116 |
16384 | 81591145 | 52602008 | 0.608013 |
32768 | 326370999 | 210450762 | 0.607969 |
∞ | 6 | π2-6 | 0.607927 |
着々と収束している様子が伺えます。
まぁ、このプログラムは、これで良いのだが、finの値が大きくなると遅いというのを実感出来るかと思う。
さて、プログラミングに精通しているのであれば、ユークリッドの互除法を、再帰処理でプログラミングするんだろうと思ったのではないでしょうか?
残念でした。
ユークリッドの互除法を使わずに、互いに素なm, nだけが現れる再帰処理によるプログラミングです。
私のブログを読んでいる方であれば、いくつも記事が上がっていますね。
coprime_recursive.c
#include <stdio.h>
#include <stdlib.h>
unsigned int true=0, fin;
void recursive(unsigned int m, unsigned int n)
{
if ( m >= fin ) return;
true++;
recursive(m*2-n, m);
recursive(n*2+m, n);
recursive(m*2+n, m);
}
int main(int argc, char *argv[])
{
if ( argc!=2 ) return EXIT_SUCCESS;
fin = atoi(argv[1]);
recursive(2, 1);
recursive(3, 1);
printf("true=%u\n",true);
return EXIT_SUCCESS;
}
recursive というサブルーチンで再帰処理していることがわかります。
こちらも、プログラム名を coprime_recursive とすると、
coprime_recursive 100
のように、第一引数で fin の値を設定します。
私の研究で見つけた互いに素の3進木構造ということで、再帰処理との親和性が非常に高いということです。
finの値が小さいうちは、どちらのプログラムもそれほど問題なく瞬殺、秒殺で求まるかと思います。
しかし、finの値を少しずつ大きくして試してみると、先の表が示した通り、
coprime_loop は true+false の回数だけループしており、
coprime_recursive は true の回数だけ再帰しているということです。
但し、再帰処理が全てにおいて勝っているとはいうわけではありません。
まず再帰処理とは切っても切り離せない、スタックオーバーフローという問題があります。
当然、全事象を調べないことには、false、つまり互いに素ではない場合のカウントもしないと、ratio、つまり確率を求めることが出来ません。
因みに、数学的に確率を求めるには、
ある自然数が素数pを約数に持つ確率は、1/p となります。
2つの自然数ですから、1/p2 となります。
素数pを約数に持たない確率は、全事象の1から引いて、1-1/p2 となります。
素数全てにおいてなので、Π{all p} 1-1/p2 となります。
計算式は、
(1-1/22)(1-1/32)(1-1/52)(1-1/72)(1-1/112)(1-1/132)(1-1/172)(1-1/192)…
ということです。
素数判定なんてやってらんねぇって考えるのであれば、
π2/6≒1/12+1/22+1/32+1/42+1/52+1/62+1/72+1/82+1/92+1/102+…
というのがありますので、これの逆数で求めることも出来ますね。
どっちにしても、事前に証明をしておく必要があるということですね。
ブログの文字数制限に引っかかるので、証明はここではしません。
プログラミングは面白いねぇ。
ではでは