はむ吉(のんびり)の練習ノート

プログラミングとことばに関する話題を中心に,思いついたこと,試してみたこと,学んだことを,覚え書きを兼ねてまとめます.その際に役立った,技術書,参考書,辞書,機器などの紹介も行います.

素数判定・素数列挙を行う主なアルゴリズムをPython 3で実装してみた:試し割り法からMiller-Rabin素数判定法まで

競技プログラミングにおいては、素数判定(ある自然数素数か否かを判定する処理)あるいは素数列挙(ある範囲に含まれる素数を列挙する処理)が求められることがあります。そこで、本記事では、素数判定・素数列挙のための代表的アルゴリズムを挙げるとともに、私がそれらをPython 3で実装したものを示します。同様の事項について書かれた記事はすでに多数ありますが、ここでは学習の記録を兼ねて自分なりにまとめておきます。

きっかけ

最近、私は競技プログラミング用のライブラリを作成し始めました。とはいうものの、体系的に作成しているというよりも、むしろ、問題演習を通じて新たなアルゴリズム技法に出会う度に、それをライブラリに追加しているというところです。しかしながら、素数判定・素数列挙にまつわる主要なアルゴリズムについては、まとめて実装してライブラリに加えました。その主な理由としては次の三つが挙げられます。第一の理由は、先ほど述べたように、競技プログラミングにおいて素数に関する処理が必要となることは、決して稀ではないということです。それゆえ、あらかじめ素数判定・素数列挙のライブラリを用意しておくことは重要であるといえます。第二に、競技プログラミングにおける数学的事項のうちで、素数をはじめとする数論的事項は私にとってどちらかというと馴染みのあるものであるということが挙げられます。たとえば、競技プログラミングではグラフ理論とそれにまつわるアルゴリズムがよく利用されますが、私がグラフ理論を知ったのは競技プログラミングを始めてからです。これに対して、素数や約数・倍数といった数論的事項には、競技プログラミング以前の学習で触れる機会がありました。第三の理由としては、どういうわけか素数は興味深いものであると感じるということがあります。Twitterでも言及しましたが、私は数学が得意でもなければ、大好きであるというわけでもありません。しかしながら、なぜだかわからないのですが、素数やそれを扱うアルゴリズムには心惹かれるものを感じるのです。もしかすると、数年前に読んだ『虚数の情緒』という本の影響によるものかもしれません。

注意事項

  • 以下に示すソースコードの作成にあたっては不具合のないよう注意しましたが、それでも問題が残っている可能性は否定できません。
  • プログラミング言語によっては、素数判定・素数列挙に利用できるクラスやモジュールが標準で備わっています(例:JavaのBigInteger、Rubyのprime)。そのような場合には、わざわざアルゴリズムを実装する必要はありません。

主な素数判定・素数列挙アルゴリズムとその実装例

試し割り法

試し割り法は、素数判定用の単純な決定的アルゴリズムです。この方法では、素数判定の対象である自然数n(>2)を2から\sqrt{n}までの自然数で割ります。割り切れるものが一つも存在しなければn素数であり、さもなければn素数ではありません。この手法の時間計算量はO(\sqrt{n})です。


Eratosthenesの篩と区間篩

Eratosthenes(エラトステネス)の篩は、n未満の素数を列挙するのに利用できる決定的アルゴリズムです。その各ステップはwikipedia:en:Sieve_of_Eratosthenesに詳述されています。同記事によると、この手法の時間計算量はO(n\log \log n)であるといいます。これは自然数n一個の素数判定に要する時間計算量ではなく、n未満の全素数の列挙に必要なそれであることに注意してください。以下の実装では、Python標準ライブラリのarrayを用いて、空間計算量の削減を図っています。

この方法の亜種として、区間篩というものがあります。これは、区間[2, \sqrt{b})素数表を用いて、[a, b)素数を列挙するという方法です*1


Miller-Rabin素数判定法

Miller-Rabin(ミラー–ラビン)素数判定法は、与えられた自然数n素数判定を行うためのアルゴリズムです。ここまで述べてきたアルゴリズムとは異なり、この手法は確率的アルゴリズムあるいは乱択アルゴリズムです。Miller-Rabin素数判定法では、[1,n-1]の範囲から擬似乱数を用いて自然数aをランダムに選び、これを用いた判定を行うという操作をk回行います*2wikipedia:ミラー–ラビン素数判定法によれば、この方法の時間計算量はO(k\cdot \log ^3 n)であるといいます。また、同記事によると、この方法で合成数を誤って素数と判定する確率は最大で4^{-k}であるといいます。

自然数nがあまりにも大きく、上述の手法では制限時間内にその素数判定ができないときに、Miller-Rabin素数判定法の利用を考えるとよいでしょう。とはいえ、このアルゴリズムを使わなければならないような問題は、めったに出されないようです*3


補足

上で挙げたアルゴリズム以外にも、素数判定・素数列挙のアルゴリズムとしてはAKS素数判定法やAtkin(アトキン)の篩などがありますが、この記事ではそれらについては触れません。

感想とまとめ

素数判定・素数列挙のアルゴリズムについてはかねてから名前程度は知っていましたが、(文献のコード例を参照しながらではありますが)自分でその実装コードを記述するのはおそらくこれが初めてのことでした。今後は問題演習を行いながら、数論にまつわるものにとどまらず、競技プログラミングで必要となる種々のアルゴリズムを実装したいと考えています。

また、この記事では実装したコードの性能評価(例:計算速度の実測)は行いませんでしたが、できれば後日やってみたいと考えています。

参考文献

上で示したソースコードの作成にあたっては、以下の文献を参考にしました。詳細な参照箇所は、ソースコード中のコメントに記しています。

*1:プログラミングコンテストチャレンジブック』(第2版)p.113によります。

*2:wikipedia:ミラー–ラビン素数判定法によります。

*3:『最強最速アルゴリズマー養成講座』p.409によります。ただし、以前にyukicoderにおいてMiller-Rabin素数判定法の利用が想定解である問題が出されたことがあります。