プログラミングコンテスト第1回「素数発見」競技の記録の伸びは飛躍的で、 コンテスト創設時に示した管理人の参考記録から10000倍の速度にまで達している。 同じコンピュータでありながらプログラムを変えただけで、これだけの性能アップが実現された秘密はどこにあるのだろうか。 この秘密について詳しく調べてみよう。 アルゴリズムの解説だから、プログラミング言語にはとらわれないつもりだが、 一部にプログラミングの話が出てくることは避けられないのでお許しを得たい。
素数という数を学習したとき、先生が 「1から100までの素数を探しなさい」 と生徒に課題をだしたとしよう。
このとき、小さい数から1つずつ素数を見つけて行く方法を取る真面目な生徒がほとんどだろう。
それと対照的な生徒もいるはずだ。
その生徒の方法とは、
1から100までの数を全て紙に書きだす。
次に、1を消し、2より大きい2の倍数(偶数)を消し、3より大きい3の倍数を消し、5より大きい5の倍数を消し....
と1から100までの数から倍数を次々に消してゆき、最後に残った数(素数)を一気に集める方法である。
この方法は最初のうちは作業だけで素数が求まるわけではない。
しかし、最終的には一番速く全ての素数を求めることができるのだ。
下に示した例は、1から100までの数から、素数でない「1」と、2より大きい「偶数」を除去した段階の表である。
これに続いて、3以外の3の倍数、5以外の5の倍数...と
順に倍数を除去してゆく作業を繰り返すだけだ。
この場合、除去するときに、掛け算、割り算を一切使わずに済むことである(その数を加えてゆくだけだから、足し算のみで済む)。
12 3456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
この作業の手順をアルゴリズムとして記述してみよう。 前回取り上げた「逐次余り判定」 のときと同様に、 まず最初に、作業を箇条書きして作業手順を表し、 次に繰り返し作業の共通部分を見つけ出す方法をとり、 「倍数除去」 アルゴリズムを記述してみよう。
素数を求める最大値を1000とする(いくらでもよいのだが)。
次々と倍数を除去してゆく作業を繰り返すだけだから、アルゴリズムは簡単なものになりそうだ。
上の手順から 「繰り返しの部分」 を抜き出してみる。
考える上で、繰り返しの起点を、 「5」 の倍数除去処理が終わったときとしよう。
表の数「5」の次の数「6」は2の倍数、3の倍数のときの2回にわたって既に消去されているから表には無い。
表に残っている次の数は「7」である。
これが次の素数である。これに気付けば、繰り返しの部分のアルゴリズムは簡単に作り上げることができる。
繰り返しの部分は非常に簡単になってしまった。 上記の処理を 「素数チェック処理」 と呼ぶことにしよう。 次に、「素数チェック処理」を繰り返し実行して、次々と素数を求める部分のアルゴリズムを作ればよい。 素数チェックの最大数の大きさを1000として、素数発見アルゴリズムを完成してみよう。
これで「倍数除去」アルゴリズムのプロトタイプが出来上がったことになる。
このアルゴリズムをプログラミング言語で記述すれば、
素数発見プログラム(倍数除去アルゴリズムによるバージョン1)が完成する。
プログラミング上で表を表現するためには、配列(array)を利用すればよい。
配列とは、変数をひとまとめにする。それぞれの変数に番号をつけて操作する変数の集合体のことである。
表の数字を先頭から T(1)、T(2)、T(3)、T(4)、.... 、T(1000) とし、それぞれの変数に1が入っておれば
表にその数字が書かれているとし、0が入っておれば、その数が表に無いとするのだ。
BASIC言語で配列を使うときには、 DIM T(1000) とプログラムの最初に記述する。
これだけでこの配列が使えるようになる。
なお、C言語では int T[1001]; 、Pascal言語では T: array[1..1000] of integer; など、
プログラミング言語により微妙に異なる表現になるが、ほとんど同一といっても良い。
センター試験で出題されるプログラミング言語であるBASIC言語を使って、
この表を記述し、アルゴリズム通りに作ったものが次に示すプログラム例である。
100 '素数発見プログラム プロトタイプ BASIC言語での記述 110 DIM T(1000) ---数の表を配列でとる 120 FOR I=1 TO 1000: T(i)=1: NEXT ---表に数を書いたことにするため配列に1を全て入れる 130 T(1)=0 ---表から1を取り除いたことにするため T(1) にゼロを入れる 140 FOR I=2 TO 1000 ---2から初めてその数より1つ小さい数までチェックを繰り返す 150 IF T(I)=0 THEN GOTO 200 ---表から既に消えているので1つ大きい数に変える 160 J=I ---その素数をセットし 170 J=J+I ---その素数を順に足すことで倍数を求め、表から消したことにするため T(J)=0 とする 180 IF J > 1000 THEN GOTO 200 ---最大値を越えると倍数除去作業を終了 190 T(J)=0: GOTO 170 200 NEXT I 210 FOR I=2 TO 1000 220 IF T(I)=1 THEN PRINT I ---T(I)=1である表に残った数を画面に表示(ファイルに書き出す) 230 NEXT I ---調べる最大数 1000 で終了 240 END
同じく、C言語で記述したものが次のプログラムである。
// 素数発見プログラム プロトタイプ C言語での記述 #include < stdio.h > int main(int argc,char **argv) { int i,j; int t[1001]; ---表を表す配列領域を作る for( i = 1; i <= 1001; i++ ) t[i]=1; ---表に数を書いたことを表す1を配列に入れる t[1]=0; ---1は素数でないことを示す0を入れる for( i = 2; i < 1001; i++ ) { ---2から順に倍数を除く作業を繰り返す for(j=2; j <= 1001; j+=i ) t[j]=0; ---倍数の除去 } for( i = 2; i < 1001; i++ ) { ---倍数を除かれた表から素数を取りだす if( t[i] ) printf("%d",i); ---素数を画面に表示 } }
Pascal言語で記述したものが次のプログラムである。
{ 素数発見プログラム プロトタイプ Pascal言語(Delphi)での記述 } program ProtoType-1; var i,j: integer; t: array[1..1000] of integer; begin for i:=1 to 1000 do t[i]:=1; ---表の数全てを書き込んだとして1を代入する t[1]:=0; ---1は素数でないので0を入れ、表から消す for i:=2 to 1000 do begin j:=i+i; while j<=1000 do begin t[j]:=0; j:=j+i; ---素数の倍数を表から消す(0を代入) end; end; for i:=2 to 1000 do ---最初の素数を3とする begin if t[i]=1 then writeln(i); ---表に残っている素数を画面に出力する end; end.
「倍数除去」アルゴリズムは、表を使うことで割り算をまったく使わない素数発見処理を可能とする。
このため、時間がかかる割り算をプログラムから排除できるため、素数発見処理を非常に速くできる。
処理速度を競うプログラムコンテストでは、ほとんどがこのアルゴリズムをベースにしたプログラムである。
アルゴリズムからプログラムに作り上げることが出来た後は、プロラムのチューニング作業に入る。 「倍数除去」 アルゴリズムで作成されたプログラムにも、「逐次余りチェック」アルゴリズムのときと同様に、 無駄な処理がいたるところに数多く含まれているはずだ。 それの無駄な処理を見つけて、細部にわたるチューニングを行うことで、 より速いプログラムに成長させることが出来る。 1000万未満までは、プログラムが思い通りに高速で動き、 プログラムのチューニング作業をしてゆくたびに記録を更新できる。 プログラミング作業に充実感がある楽しい時期だ。 しかし、上の記録を狙うため素数発見の限界を大きくしてゆくことになる。
チューニング作業でプログラムの速度を上げることは難しくない。どこまでも速くできるのか?
限界値をどこまでも大きく出来るのか?と記録を目指せば、壁に突き当たる。
このアルゴリズムにも 「大きな弱点」 が存在する。
その弱点が顕著に現れてくるのは「1億未満」部門のように素数限界値が大きくなり、
使う表が大きくなったときである。
コンテストの記録を見ても、3月中旬までは1000万未満部門にはそれぞれ参加できてはいたが、
1億未満の部門には参加できなかった(1億未満は実行不可能?時間がかかりすぎた?からか)。
3月18日になって、「KITAKEN」君が初めて「1億未満」部門に登場する。
「影武者」さんは3月12日には「5億未満」部門の記録を出しているのに!
なぜそのような違いが現れたのか。
それは、このアルゴリズムの弱点を克服する方法を「影武者」さんが早く発見できたということだ。
「1億未満」部門以上に参加するにはこの弱点を克服する技が必要になる。
それが出来ないうちは、1億未満部門に参加できないのだ。
理論的には簡単に次の段階に行けるように思えるが、
突然に信じられないくらいにプログラムの処理速度が低速になってしまう。
プログラムがいつまで経っても終わらない状態に陥るのだ。
「影武者」さん、「ことり」君、「rouden」君、「KITAKEN」君および、
プログラム作りに挑戦していた「影のコンテスト参加者」のプログラマさん達すべてが
経験した大きな壁だった。
この速度低下現象が、「倍数除去」 アルゴリズムが持つ最大の弱点なのだ。
「表」 そのものに起因する弱点だから重大な問題点である。
アルゴリズム上で必須のものである 「表」 が原因となっている。
表をやめるわけには行かないから、このアルゴリズムでは致命的な弱点になる。
なぜ、プログラムが低速化するのか、その原因は何かを知り、 その状態に陥らないようにする対策にはどのような方法があるのかなど、 この弱点克服については、コンピュータの仕組みを含めて多方面の知識が必要になる。 次回は、この弱点克服について、そのための色々な技の詳細を説明する。 また、これらの技を使ってどのように弱点を克服してゆくかを解説する予定である。
「第12回 「素数発見」アルゴリズムの進化を解説する B」 を 見る。