# エラトステネスの篩

エラトステネスでググったら出てきて、さすがにカオス過ぎて笑ってしまいました。 エラトステネスの篩というのは n 以下の整数の素数を求めるアルゴリズムです。

原理自体はとても簡単で、たくさんの数を用意して、次にひたすら割り続けます。 割ることができた数は振い落とします。

以下では 1 から 169 の数を用意して 1, 2, 3, 5, 7, 11, 13 と素数で割り続けて素数を見つけます。

1 ~ 169 の数字をご用意しました。 これをエラトステネスの篩にかけていきます。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169

4つの書き方を比較検討しました。

項番 項目 時間
1 簡単なもの 9.0777 [msec]
2 高速化 1.6104 [msec]
3 OrderedDict 7.5887 [msec]
4 dict 5.8195 [msec]

計測に使用したコードはこちら。

git clone https://github.com/niconico25/comparison_of_python_code/
cd comparison_of_python_code
python3 9_2_sieve_of_eratosthenes.py

# 1. 簡単なもの

def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = False
    is_prime[1] = False
    for i in range(2, n + 1):
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]

参考にしたサイトは、下記のサイトから抜粋しました。

# 2. 高速版

5 倍くらい早くなる。

def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = False
    is_prime[1] = False
    for i in range(2, int(n**0.5) + 1):
        if not is_prime[i]:
            continue
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]

for 文の方が while 文よりも速い気配があるので、for 文で書きました。 その検証は下のサイトでやりました。for 文の方が綺麗に書けますしね。 素因数分解するときは while 文の方が早かったです。なぜかは、わかりません。

# 3. OrderedDict

# ◯ 背景

枝狩りをした 2. 高速版のアルゴリズムでも、 既に素数でないと判定されたものにも重複して、素数でないと判定しています。 同じ j に対して複数回 is_prime[j] = False を実行しています。

例えば 3 * 4, 3 * 6, 3 * 8 のような数でそのような重複がなされています (素数 * 既に素数と判定された数の倍数)。

そこで順序付き集合を用いて、 同じ処理を繰り返し行わないようにしたいと考えました。

# ◯ 速度

しかしそこまで速くならず、簡単なものより 1.5 倍速いくらい。 アルゴリズム的には一番速いはずだけど。

Python は、四則演算やらなんやらが遅いので、 何が原因かはいまいちよくわかりません。

# ◯ 本当に重複してないの?

素数の判定を重複して行いません。1度だけしか行いません。 本当にそうなの?みたいな感じですが、重複して削除していた場合 del number_dict[non_prime] で例外を投げ返されます。

# ◯ non_prime_list

その時は for 文の中では del number_dict[non_prime] ができません。 できいないので上のコードでは non_prime_list を用いて 一時的に素数ではない数をリストとして保存している。

# ◯ ポイント

ポイントは以下の箇所です。 ここの動作は上のアニメーションと照らし合わせると、 追いやすいかなと思います。

def primes_ordered_dict(n):

    ...

        # 次の約数 = prime X prime
        #
        # なので、もし prime X prime が n より大きくなったら break
        if not (prime * prime <= n):
            break
        non_prime_list.append(prime * prime)

        # 約数 = prime X k
        # k は prime より大きいまだ篩い落されていない数
        for k in number_dict:
            if prime * k <= n:
                non_prime_list.append(prime * k)
            else:
                break

# ◯ コード

Python 3.6 以前の set は Ordered, 順序付きではないので OrderedDict で代用しました。

import collections


def primes_ordered_dict(n):
    #
    prime_list = []
    non_prime_list = []
    number_dict = collections.OrderedDict(
        ((i, None) for i in range(n + 1)))
    #
    del number_dict[0]
    del number_dict[1]
    while True:
        # get prime
        prime, _ = number_dict.popitem(False)
        prime_list.append(prime)
        # break
        if not (prime * prime <= n):
            break
        # get non primes
        non_prime_list.append(prime * prime)
        for k in number_dict:
            if prime * k <= n:
                non_prime_list.append(prime * k)
            else:
                break
        # delete non primes
        for non_prime in non_prime_list:
            del number_dict[non_prime]
        non_prime_list.clear()

    # add rest of numbers as prime
    prime_list.extend(
        [prime for prime, _ in number_dict.items()])

    return prime_list

# 4. dict

Python 3.6 以前では辞書は順序が保たれていなかったのですが、 Python 3.7 から順序付きになりました。

What’s New In Python 3.7
Python data model improvements:

  • the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec.

書いてみたのですが、そこまで速くできませんでした。 while 文を多用せざるを得ず、結果的に Python のコードが長くなってしまったためです。

Python は遅いので C で実装されているコードに処理を投げられれば大抵速くなります。 逆に言えば Python で書くコードが長くなると遅くなってしまいます。

なぜ while 文で書いていたかというと、 for 文だと正順 (FIFO) で要素を取り出すのに dict.popitem だと逆順(LIFO)で要素を取り出すためです。

>>> reversed({})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'dict' object is not reversible
>>> 

popitem()
任意の (key, value) 対を辞書から消去して返します。 対は LIFO の順序で返却されます。

以下が実装したコードです。

def primes_dict(n):
    #
    prime_list = []
    non_prime_list = []
    k_list = []
    number_dict = {
        i: None for i in reversed(range(n + 1))}
    #
    del number_dict[0]
    del number_dict[1]
    while True:
        # get prime
        prime, _ = number_dict.popitem()
        prime_list.append(prime)
        if prime * prime > n:
            break

        # get non primes
        non_prime_list.append(prime * prime)
        while True:
            k, _ = number_dict.popitem()
            k_list.append(k)
            if prime * k > n:
                break
            non_prime_list.append(prime * k)

        while k_list:
            number_dict[k_list.pop()] = None

        # delete non prime
        while non_prime_list:
            del number_dict[non_prime_list.pop()]

    while number_dict:
        prime, _ = number_dict.popitem()
        prime_list.append(prime)

    return prime_list

ちなみにこの更新は、日本人の方がコミットされています。 初めて知ったときは驚きました。

# 5. ワンライナー

これはエラトステネスの篩では、ありません。速度的には 1 より少し遅いくらいでした。 自分のパソコンだと i=3 を代入すると計算が終わらない。しかし、こんなやり方、思いつくなんて凄すぎる...。 Twitter の "だよ〜" ってコメントなんだよ、軽すぎるよ (´;ω;`)ブワッ

# 素数リスト 2**i 桁以下
primes = lambda i: primes(i - 1) + [k for k in range(10**2**(i-1), 10**2**i) if all(k % p for p in primes(i - 1))] if i else [2, 3, 5, 7]
# 素数リスト 2**i 桁以下
def primes(i):
    if i:
        # 素数リスト 2**(i-1) 桁以下
        L = primes(i - 1)

        # 素数リスト 2**(i-1)+1 桁以上 2**i 桁以下
        n = range(10**2**(i-1), 10**2**i)
        M = [k for k in n if all(k % p for p in L)]

        # 素数リスト 2**i 桁以下
        return L + M
    else:
        # 素数リスト 2**0 桁以下
        return [2, 3, 5, 7]

# ◯ ポイント

def primes(i):
        ...
        # 素数リスト 2**(i-1)+1 桁以上 2**i 桁以下
        n = range(10**2**(i-1), 10**2**i)
        M = [k for k in n if all(k % p for p in L)]
        ...

2**(i-1)+1 桁以上 2**i 桁以下の合成数(素数でない数)は、 2**(i-1) 桁以下の素数を素因数に持ちます。逆に言えば、2**(i-1)+1 桁以上 2**i 桁以下の合成数(素数でない数)は、2**(i-1)+1 桁以上の素数を素因数に持ちません。

2 桁の合成数は、1 桁以下の素因数を持ちます。3 桁以上4 桁以下の合成数は、2桁以下の素因数を持ちます。5 桁以上 8 桁以下の合成数は、4 桁以下の素因数を持ちます。

例えば 2 桁の合成数で 1 桁の素因数を持たない数を考えて見てください。存在しません。逆に言えば、例えば 2 桁の素数を使って 2 桁の合成数を作って見てください。作ることはできません。

これは、任意の 2 桁の素数 p, q について考えると、必ず p * q >= 100 となるからです。