プログラミングコンテスト第1回「素数発見」競技の記録の伸びは飛躍的で、 コンテスト創設時に示した管理人の参考記録から10000倍の速度にまで達している。 同じコンピュータでありながらプログラムを変えただけで、これだけの性能アップが実現された秘密はどこにあるのだろうか。 この秘密について詳しく調べてみよう。 アルゴリズムの解説だから、プログラミング言語にはとらわれないつもりだが、 一部にプログラミングの話が出てくることは避けられないのでお許しを得たい。
素数とは 「1と自分自身で割り切れない自然数」 という定義である。素数発見のプログラムは、
それにもとづいたアルゴリズムの組み立てから始まる。
アルゴリズムの組み立ては、小、中学生が素数を見つけ出すときにする手順を、
具体手金考えることで書き表すことが出来る。
誰でも思いつく方法には、次に示す2つの方法があげられる。
1つ目の方法は 「定義のまま正直に素数を見つける」 (逐次余り判定)を使う方法
2つ目の方法は 「自然数の表を作って倍数を除去する」 (倍数除去)を使う方法
の2つになるだろう。
この方法は誰でも最初に思いつく方法である。この方法での素数発見のアルゴリズムを作り上げてみよう。
自然数の集合の先頭から調べてゆくとしよう。当然、定義に含まれる1と自分自身以上の自然数は除くいてよい。
したがって、次のように示す手順が取られることになる。
2の場合は割り算する数がない1より大きく自分自身より小さい数はないので、素数である。
したがって、定義に基づいて調べる数は3からになる。
上の手順をもとに素数発見アルゴリズム「プロトタイプ」を作り上げてみよう。
上の手順には繰り返しのパターンが存在しており、その繰り返し部分(共通部)を考えて取り出してみる。
繰り返しのパターンとは次のものになる。
上記の処理を 「素数チェック処理」 と呼ぶことにする。 次に、「素数チェック処理」を繰り返す部分を作ればよい。 例えば 素数チェックの最大数の大きさを 1000 までとして、素数発見アルゴリズムを完成してみよう。
これでアルゴリズム「プロトタイプ」が出来上がったことになる。 このアルゴリズムをプログラミング言語で記述すれば、素数発見プログラム@が完成する。 具体的な例として、センター試験で出題されるプログラミング言語であるBASIC言語で記述したものが次に示すプログラムである。
100 '素数発見プログラム プロトタイプ@ BASIC言語での記述 110 PRINT 2 ---素数2を画面に表示 120 FOR A=3 TO 1000' 130 FOR B=2 to A-1 ---2から初めてその数より1つ小さい数までチェックを繰り返す 140 '素数チェック処理 150 IF (A MOD B)=0 THEN GOTO 200 ---余りがゼロだから素数でない 170 NEXT B 180 PRINT A 200 NEXT A ---調べる最大数 1000 以下ならチェックに戻る 210 END
C言語で記述したものが次のプログラムである。
// 素数発見プログラム プロトタイプ@ C言語での記述 #include < stdio.h > int main(int argc,char **argv) { int a,b,f; printf("%d\n",2); ---素数2を画面に表示 for(a = 3; a <= 1000; a++) { ---3から最大1000までの素数をチェックする f=1; b=2; while(b < a) { if(a%b == 0) { f=0; break; } b++; } if(f) printf("%d\n",a); ---割り算の余りがゼロなら次の数へ } }
Pascal言語で記述したものが次のプログラムである。
{ 素数発見プログラム プロトタイプ@ Pascal言語(Delphi)での記述 } program ProtoType-1; var a,b,f: integer; begin for a:=3 to 1000 do ---最初の 「数」 を3とする begin f:=1; b:=2; while( b < a ) do ---2から初めてその数より1つ小さい数までチェックを繰り返す begin if (a mod b) = 0 then begin f:=0; break; end; b:=b+1; end; if f <> 0 then writeln(a); ---素数だから、画面に表示する end; ---調べる数を1増やす end.
「逐次余り判定」アルゴリズムでの素数発見プログラムでは、良い記録は出せない。
非常に遅くなる(時間がかかる)理由は、コンピュータにさせる仕事が多くなるためである。
このアルゴリズムでのプログラムから処理の無駄を見つけて、
それを取り除いてゆくと、処理速度がどんどん速くなってゆく。
この作業をアルゴリズムの 「チューニング作業」 という。
チューニングを行うには、処理に時間がかかっている部分を見つけ出すことから始まる。
このアルゴリズムにおける分かりやすい問題点を何点か次に指摘してみよう。
一般に割り算の計算は足し算、引き算に比べると非常に時間がかかる。
割り算(掛け算)の回数を少なくすることでプログラムの処理を速くすることが出来る。
このプログラムでは、「素数チェック処理」 の中で、繰り返して割り算が実行されている。
割り算の回数を減少させることがポイントになる。
そこで、「素数チェック処理」 を行う回数を減らすことを考えてみよう。
割り算そのものを全て失くすこと、割り算そのものの計算を早くすることについては、この場では考えないものとする。
だれでも気付く無駄な「素数チェック処理」に、偶数は2以外は全て素数でない! ことにある。
上のプログラムにおいて偶数も含めて全て素数チェック処理を行っている。
すなわち半分のチェックは無駄なチェックである。
したがって、奇数のみをチェックするようにアルゴリズムを改善すれば速度は2倍になる(単純だが)。
それ以外に大きな無駄はないだろうか?ある人は3の倍数、5の倍数、7の倍数....などと
すべての倍数を全て除けば良いなどという。これができるなら素数発見は簡単にすむ。
そのためにプログラムはどんど複雑になり、それを実現させることが出来なくなることが想像される。
プログラムの作成の手間を考えると、どこかで倍数除去を打ち切る必要がある。
プログラミングの労力から考慮した場合その分岐点は3の倍数くらいではないだろうか。
意外なところに素数チェック処理の大きな無駄が隠れている。
前述の偶数をチェックしていた無駄よりはるかに大きな無駄がある。しかし、なかなか気付かない無駄の部分でもある。
プログラムにおける無駄な処理の部分を発見するには具体的にプログラムの進行を追ってみることが大切だ。
1つ例を出してみると、997は素数であるが、この数が素数であるかどうかをチェックする処理の過程を考えてみよう。
997未満の数、2、3、4、5、6、... と割り算して、996まで割り算チェック全てが本当に必要なのだろうか。
これについて詳しく考えてみよう。2、3まで割り切れないことが分かったとしよう。
次に調べる4での割り算だが、当然そのとき4では割れない。奇数は偶数では割り切れないのだから当然だ!
したがって、4で割り算すること自体が無駄(同様に、6、8...など偶数すべて同様)である。
この無駄は、2の倍数になる数は割り算する必要がなくなったことになるから、
素数チェック処理での割り算が 「奇数」 だけでよくなった点だ。
これをアルゴリズムに取り込めば、素数チェック処理を2倍速にスピードアップできる。
前述の無駄と合わせて、これで4倍速になるチューニングがなされたことになる。
しかし、これだけだはなさそうだ。まだまだ無駄な処理はいたるところに隠れているのだ。
997について考えてみよう。アルゴリズムによると、2、3、4、5、6、... 995、996 と順番に割り算してゆくことになるのだが、
全て割り算を実行する必要があるのだろうか? 例えば、501で割り算したとき、503で割り算したときを考えると、
その疑問点に気付くはずだ。
449以上の数では全ての場合に割り切れないことが明らかである。この点に気付くだけでも大きな進歩である。
もっと深くこの点を考えると問題点は小さな無駄でないことに気付くのだ。
「997の平方根までの数で割り切れない場合、997の平方根より大きな数では絶対に割り切れない」 という事実に気付くのが。
このことをアルゴリズムに組み込んだ場合、大きな効率アップが得られる。
例えば、1億未満の場合、平方根は100万だから、100万までの割り算を実行するだけで素数チェック処理が終わってしまうことになる。
これは全てチェックするときに比べて(1億ー100万)=9900万回の割り算が省略されるので、
単純には100倍速という驚異的な改善につながってくるのだ。
考えれば考えるほどプログラムの中には無駄な計算処理が隠れているかが分かって来る。
プログラムの記述能力は、単なる作文ではない。プログラムを記述する主体であるアルゴリズムで良いのか悪いノアk。
そのアルゴリズムがどの程度効率的なのか?無駄が多く含まれているのかいないのか?など
アルゴリズムの評価がいかに大切であるかは、このコンテスト「素数発見」競技における記録の驚異的な伸びを通してよく分かるだろう。
プログラミングの能力とは、プログラム言語の学習で全て体得できると思っている人が多い。 プログラム言語の体得は1ヶ月もあればだれでも身につくものだ。 プログラムにおいて出てくる単語は僅か100程度で、 数1000語の単語が必要とされる英語よりはるかに覚えるのは簡単である。 英語や日本語の文法、単語だけを学習して、立派な論文や小説が書けると思う人はいない。 論文や小説はその人の頭の中の知識の集大成なのだから、言語の問題ではない。 プログラミング能力も、論文や小説を書くのと同様である。 プログラマの幅広い知識、プログラム対象を分析する能力などにより総合的に作り上げられるものがプログラムなのである。 このことは、今回の素数発見プログラミングを通してよく分かるだろう。 プログラミング言語は単にアルゴリズムを表す手段であるだけで、 プログラムを作る作業でのプログラム言語の役割は、優れたアルゴリズムを忠実に表現しやすい言語であるかどうか ということである。 アルゴリズムそのものは考え出す能力を持たない優秀なプログラマはいないのだ(総合力が全ての基本の世界)。
「逐次余り」アルゴリズムでは、計算部分の無駄を見つけて、これを取り除く方法で処理時間を短くすることができた。
しかし、割り算の計算をさせる限り、計算効率の上昇には限界がある。
更なる記録を目指す場合には、新たな「素数発見」のアルゴリズムを作り出す以外にはない。
割り算をさせずに、素数を判定するアルゴリズムを考えなければならないのだ。
このアルゴリズムが 「倍数除去」 アルゴリズムであり、コンテスト応募者は全てこのアルゴリズムを採用している。
次回は、この高速アルゴリズムである 「倍数除去」 アルゴリズムについて詳しく説明し、
このアルゴリズムを使った場合における限界を考えて見ることにしよう。
「第11回 「素数発見」アルゴリズムの進化を解説する A」 を 見る。