複天一流:どんな手を使ってでも問題解決を図るブログ

宮本武蔵の五輪書の教えに従い、どんな手を使ってでも問題解決を図るブログです(特に、科学、数学、工学の問題についてですが)

東大数学2024問題6 (part 3): エラトステネスの篩(ふるい)と数値計算で扱える最大の整数

前回のあらすじ

前回のブログでは、The PrimePagesで公開されている素数のデータベースをダウンロードし、これを読み込む部分を(pythonを用いて)書きあげるところまで完了した。

今回は、いよいよエラトステネスの篩と呼ばれる、古来より知られる有名なアルゴリズムを用いて、最初の10000個(1万個)の素数を計算してみようと思う。(計算結果は、The PrimePagesからダウンロードしたデータベースと比較し、正しく計算できたか確認する計画。)

エラトステネスの篩

高校数学でやったという記憶はないが、最近の高校の教科書では紹介されているようである。 うろ覚えながら、その手順は次のような感じとなる。

  1. 0は除く。
  2. 1は除く。
  3. 素数を見つけたい範囲を決める(たとえば0から100までの中に含まれる素数、といった具合)
  4. 2が最初の素数
  5. 最初の素数(2)の倍数を、素数を見つけたい範囲から消す(2,4,6,8,...100、つまり100以下の偶数を消す)。
  6. 4.のプロセスで消去されなかった最初の数が、次の素数となる(3)。
  7. 2番目の素数(3)の倍数を、素数を見つけたい範囲から消す(3,6,9,.....,99)。
  8. ここまでで生き延びた最小の整数を3番目の素数とする(5)。
  9. 3-5のプロセスを繰り返し、最大の素数が100を超えたらアルゴリズムを終了する(97の次の素数が101となるので、最大の素数は97として計算終了)。

3-5のプロセスがメインの繰り返しジョブとなり、8の終了判定が重要である。

このアルゴリズムをより効率よく書き換えて、数値的に計算が楽になる有名なコードもおそらく存在するであろう。しかし、今回はまず自分でプログラムを組んでみることにする。

pythonによる実装

pythonはどの程度まで大きな整数を扱うことができるのであろうか?FORTRANやCではその最大値が決まっていて、gccの場合はlimits.hで調べることができるという。

www.gnu.org

実際にコードを書いて確かめてみた。

#include <stdio.h>
#include <limits.h>

int main(){
  printf("Min for signed int: %d\n", INT_MIN);
  printf("Max for signed int: %d\n", INT_MAX);
  printf("Max for unsigned int: %u\n", UINT_MAX);
}

実行結果は次のようになった。

Min for signed int: -2147483648
Max for signed int:  2147483647
Max for unsigned int: 4294967295

素数の計算では負数は出てこないから、unsigned intを用いることができる。となると、扱える最大の整数は4,294,967,295、つまり43億程度まで扱えるということである(裏金の帳簿管理プログラムにはギリギリ使える値だが、国家予算の帳簿プログラムには使えないかも)。この値は32ビットの最大値に対応しており、bcで確かみるとその通りである。

$ echo '2^32' | bc -ql

> 4294967296

符号付きだと1ビット持って行かれてしまうので、31ビットとなり、上の値の半分となる。

$ echo '2^31' | bc -ql 

> 2147483648

ここで実験をやってみる。最大の整数に対して、あえて1を足してみるのである。水をなみなみと注いだグラスに、一滴水を垂らすと何が起きるであろうか?たぶん、これはマシンによって結果は異なると思うが。

#include <stdio.h>
#include <limits.h>

int main(){
  int n;
  unsigned int m;

  n = INT_MAX;
  m = UINT_MAX;

  printf("Out of bounds: %d %d\n",n,n+1);
  printf("Out of bounds: %u %u\n",m,m+1);
}

ここでのポイントは、符号なし整数の識別子は%uと表す点である(今日まで知らなかった....)。結果は次のとおり。

Out of bounds: 2147483647 -2147483648
Out of bounds: 4294967295 0

どうやら、丸い地球を平らだと思って探検に出かけ、水平線を越えてみたら出発点に戻ってしまった、という感じである。

FORTRANでは整数を使った数値計算はあまりやらないのだが、たぶんC言語と同じような状況になっているはずである。特にgfortranはgccの付加体として定義されているからなおのことである。(もしかするとgfortranなら#include <limits.h>を受け付けるかもしれないと思って、やってみたがダメであった。)

いろいろとFortranについての資料を探してみたが、整数型についての説明はあまりなかった。これは当然で、科学技術計算では浮動点小数計算が主流であり、あまり整数を使った計算はやらないからだ。とはいえ、最近では素数を利用した暗号技術などが重要になってきたので、Fortranで色々計算している人も多少はいるのではないかと想像する。また、デジタル情報の処理は基本的に整数でできるから、工学系の人で整数を扱うFortranプログラミングをしている人もいるのかもしれない。とはいえ、調べた限りでは、手持ちのgfortranの整数がどの程度の範囲まで扱えるのか調べきることができなかった。

こういう時は、自分で試してみるしかない。

Fortran95で整数の最大値を確かめる

たとえば、整数の4ビット表現の場合、ビットが4つあるので \begin{equation} [a][b][c][d] \end{equation} となる。ビット表現は2進数なので、a,b,c,dは1か0の値しかとれない。たとえば、 $[1][0][0][1]$といったような感じになる。これを10進数に変換する方法は(高校数学でも解説があるように) \begin{equation} 1\cdot 2^3 + 0\cdot 2^2 + 0\cdot 2^1 + 1\cdot 2^0 \end{equation} つまり、 $8+0+0+1 =9$である。したがって、4ビットの一般的な表現を10進数に変換する公式は \begin{equation} [a][b][c][d] = a\cdot 2^3 + b\cdot 2^2 + c\cdot 2^1 + d\cdot 2^0 \end{equation} である。

4ビットの場合は、2の3乗、つまり(二進数の)3桁までが登場する。したがって、4ビット表現で表せる最大の整数は $8+4+2+1=15$となる。これは$2^4-1$とも書けるが、よくよく考えると2の等比級数(n次まで)になっているから当然である。 \begin{equation} S_n = 1+2+2^2+\cdots +2^n = \frac{1-2^{n+1}}{1-2} \end{equation} しかし、この「当然」はプログラミングにおいては「当然」ではない。なぜなら、もし4ビット表現が採用されていた場合、$2^4$を計算した瞬間にオーバーフローが発生し、そこから1を引いたとしても正しい値に戻ってきてくれるかどうかは保証されないからである。したがって、上の等比級数の公式を「鵜呑み状態」で利用するのは危険である。より安全なのは、等比級数の定義式、つまり$2^3$までの等比数列文字通り足しあげるアプローチである。

数値実験のためのプログラムは次のようなものである。

program max_int_limit
  implicit none
  integer :: n
  integer  :: r,sum
  !integer*4 :: r,sum
  !integer*8 :: r,sum
  !integer*16 :: r,sum

  sum = 0
  r = 1
  do n=1,35
     sum = sum + r
     print *,n,r,sum, sum + 1
     r = r*2
  enddo
  
end program max_int_limit

integerの宣言にはさまざまなタイプがあるようだ。デフォルトのもの、$*4, *8, *16$などがある。C言語にある「符号なし」の宣言は見つからなかった。

プログラム中の変数の意味は、$n$が次数を表し(ビット数に相当)、$r$が等比数列、そして$sum$が等比級数である。

プログラム後半のループでは、$n$を一つずつ増加させながら、整数が正しく計算され表現できているかを確かめていく。破綻した表現が出た場所が、Fortran95が扱う整数の限界(最大値)となる。最大値がどこかを確かめれば、何ビット表現になっているかわかる、という狙いである。

さて、gfortranでコンパイルし、実行してみる。デフォルト、4, 8, *16の順番でやってみる。

デフォルトのinteger :: r, sumの場合

まずは$r$と$sum$を、デフォルトのintegerとして宣言して計算した場合を見てみる。結果は次のようになった。 (出力は、$n, r_n, S_n, S_n+1$、つまり次数、等比数列級数級数に1を足したもの、の順番)

           1           1           1           2
           2           2           3           4
           3           4           7           8
           4           8          15          16
           5          16          31          32
           6          32          63          64
           7          64         127         128
           8         128         255         256
           9         256         511         512
          10         512        1023        1024
          11        1024        2047        2048
          12        2048        4095        4096
          13        4096        8191        8192
          14        8192       16383       16384
          15       16384       32767       32768
          16       32768       65535       65536
          17       65536      131071      131072
          18      131072      262143      262144
          19      262144      524287      524288
          20      524288     1048575     1048576
          21     1048576     2097151     2097152
          22     2097152     4194303     4194304
          23     4194304     8388607     8388608
          24     8388608    16777215    16777216
          25    16777216    33554431    33554432
          26    33554432    67108863    67108864
          27    67108864   134217727   134217728
          28   134217728   268435455   268435456
          29   268435456   536870911   536870912
          30   536870912  1073741823  1073741824
          31  1073741824  2147483647 -2147483648
          32 -2147483648          -1           0
          33           0          -1           0
          34           0          -1           0
          35           0          -1           0

$n=30$までは順調に計算は進んでいるが、$n=31$のとき、sum+1の値がおかしくなっている。つまり、gfortranのintegerは、32ビット表現になっていて、扱える最大の整数値は$2147483647 = 1+2+4+\cdots 2^{31}$というわけである。

$n=32$ではすでにオーバーフローしてしまい、$r$自体の値が変になっていて、級数(和)を計算してももはや何の意味もないことが確認できる。

integer*4 の場合

次にinteger4を試してみると、上の計算結果と同じとなった。つまり、デフォルトのintegerはinteger4と同じであり、この2つは32ビット表現になっているのである。

integer*8の場合

ではinteger*8を試してみよう。おそらく64bit表現になっているはずだから、nのループの最大値を65にしてみる。

program max_int_limit
  implicit none
  integer :: n
  integer*8 :: r,sum

....

  do n=1,65
     ...
  enddo

end program max_int_limit

結果はこうなった(前半部分省略)

          56    36028797018963968    72057594037927935    72057594037927936
          57    72057594037927936   144115188075855871   144115188075855872
          58   144115188075855872   288230376151711743   288230376151711744
          59   288230376151711744   576460752303423487   576460752303423488
          60   576460752303423488  1152921504606846975  1152921504606846976
          61  1152921504606846976  2305843009213693951  2305843009213693952
          62  2305843009213693952  4611686018427387903  4611686018427387904
          63  4611686018427387904  9223372036854775807 -9223372036854775808
          64 -9223372036854775808                   -1                    0
          65                    0                   -1                    0

予想通りである。$n=63$の$sum$までは計算できているが、そこに1を足すと変なこと(つまり負数になってしまう)が発生し、その後の計算は「もうダメ」状態である。つまり、integer*8は64bit表現である。「900京」をこえる整数が扱えるというのは、すごいことだ!

この上となると「垓」の世界である(アボガドロ数の世界へ近づくということでもある!)。integer*16でコンパイルエラーが出なければ、とんでもなく大きさな整数をgfortranは扱えるということになりそうである。果たしてどうだろうか?

integer*16にトライ!

予想ではinteger*16は128bit表現になるはずである。$2^{128}=10^x$とおいて$x$を計算すると \begin{equation} x = 128\log_{10}2 \sim 128\cdot 0.3010 = 38.53... \end{equation} つまり$10^{38}$の世界である!アボガドロ数のオーダーが$10^{23}$であったから、それを遥かに上回る「宇宙の果て」の世界に似た感じがする大きさの整数である。

実行結果は次のようになって、ちゃんと「宇宙の果て」の雰囲気を醸し出す巨大な整数が表示されたのであった! まちがいなく、128bitである!

ということで、どうやらFortran95で整数の研究をやっている数学者/情報工学者がいそうな感じに見えてきた。しかし、暗号資産とか電子通貨といったものはこれを超える桁での素数を研究しているはずだ。

pythonではどうなのか?

大きな素数の研究をするならFortran95でやった方がよいのだろうか?といった気持ちが芽生えてきたわけだが、果たして我々が利用しているPythonはどうなっているのであろうか?

ググってみると、どうやら「限界なし」というおそろしいことになっているらしいのである。

python-codes.net

この辺りは次回にもう少し詳しく見てみたいと思う。有用そうな資料としては次のブログも注目である。

note.nkmk.me

エラトステネスの篩のコードまでいけなかった....

コードはできているのだが、最大整数の扱いのところで知らなかったことが山ほど出てきてしまい、今回はそこで一杯一杯となってしまった。次回でPythonでの最大整数の扱いについて議論した後、その次でエラトステネスへいけるのではないかと思う。