乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

アルゴリズム

最近どこかのブログで
素数を求めるコートが載っていました。

今回はいろんなコードで
素数を求めてみます。

コードによる計算回数の違いや
速度なんかが気になってしまいました。

解説動画はこちら



さて素数ですが知らない人は
ほとんどいないと思いますが
1より大きい自然数で
正の約数が1と自分自身のみである
数のことです。

そんな素数を求めるコードを考えてみましょう。

まずは2からその素数まで愚直に割っていきます。
素数の候補は6桁の素数である999983です

これが素数かどうかを判定していきます。


1.愚直に割って求めるプログラム
def isPrime1(num):
    count = 0
    if num<2:
        return False
    for k in range(2,num):
        count+=1
        if num % k==0:
            return False
    #print(count)
    return True

%timeit -n1 isPrime1(999983)
72.9 ms ± 886 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

Jupyter Notebookでは

%timeit -n回数 コード

と書いて実行すると
そのコードを何回か実行して
その計算時間を測ってくれます。

この場合の計算回数は999981回でした。
その素数の数と同じような計算回数ですね。
非常に時間がかかっています。

つぎは計算回数を減らしてみましょう。
割るのをその候補のルートを取った
数までにしてみます。

2.計算回数を減らしてみる
import math

def isPrime2(num):
    count = 0
    if num<2:
        return False
    for k in range(2, int(math.sqrt(num)) + 1):
        count+=1
        if num % k == 0:
            return False
    #print(count)
    return True

%timeit -n1 isPrime2(999983)
66.8 µs ± 2.44 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

これで計算回数も大幅に減り
998回まで減りました。

実行速度の方もmsからμsまで減っています。
大幅な速度アップですね。

それでも割るのは無駄が多いです
2で割れたら素数では無いので
改めて偶数で割る必要は無いですよね。

偶数を抜いて割るのを奇数だけにしてみます。

3.割るのは奇数のみにしてみる
import math

def isPrime3(num):
    count = 0
    if num==2:
        return True
    if num<2 or num%2==0:
        return False
    for k in range(3, int(math.sqrt(num)) + 1,2):
        count+=1
        if num % k == 0:
            return False
    #print(count)
    return True

%timeit -n1 isPrime3(999983)
31.7 µs ± 955 ns per loop (mean ± std. dev. of 7 runs, 1 loop each)

計算回数は499
さらに半分くらいの計算回数になりましたね。

さて奇数で割るだけにしただけでも
かなり早くなりましたが、それでも
まだまだ無駄があります。

5で割り切れるなら改めて
15で割る必要は無いですよね。

割るのを奇数から
素数だけにしてみましょう。


4.あらかじめ素数を用意しておき
割るのを素数だけにする
def isPrime3_2(num):
    if num==2:
        return True
    if num<2 or num%2==0:
        return False
    for k in range(3, int(math.sqrt(num)) + 1,2):
        if num % k == 0:
            return False
    return True

prime_numbers = [i for i in range(2,999983+1) if isPrime3_2(i)]
あらかじめ沢山の素数を用意しておきます。
改めて素数だけで割ってみます。
def isPrime4(num):
    count = 0
    if num<2:
        return False
    elif num==2:
        return True

    m = math.floor(math.sqrt(num))
    for p in prime_numbers:
        count+=1
        if num % p == 0:
            return False
        if p > m:
            # 素数がnの平方根を超えたら終了
            break
    #print(count)
    return True

%timeit -n1 isPrime4(999983)
12.2 µs ± 1.35 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

計算回数も169回と大幅削減ですが
最初に素数を沢山用意しておくのがネックです

素数計算を沢山やる必要があるなら
プログラムとしては許せますが
桁が多くなったりすると対応しきれません。

そんな訳で最後の手です。

5. from sympy import isprimeを使ってみる

sympy はPythonで
記号計算を行うためのライブラリです。

その中には素数を求めるメソッドが
あらかじめ用意されています。

isprime

これを使ってみましょう。

from sympy import isprime

%timeit -n1 isprime(999983)
12 µs ± 7.28 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)


さて、計算回数は分かりませんが
実行時間は先ほどの素数をあらかじめ用意したものと
さほど変わりません。

であれば、Pythonでは
わざわざ素数を求めるような
関数とか作る必要が無いですねーーー

githubに関数のソースがあるようなので
みてみると良いかもしれないですね。
sympyのソース


ということでまとめです。
こういった素数を求める関数を作るような事を
車輪の再発明と言います。

勉強のためであれば
良いとは思いますが業務で行う場合は
時間の無駄になる事がほとんどです。

こういった課題というのは時折
コードを書かせる試験などで
用いられるようですが

入社後はあまり役には
立たないかもしれませんね。

システム開発の際は
車輪の再発明にならないように
色々と調査をしておくと
無駄が少なく工数を節約できる
こともあるので日々の情報収集は
大事ですね。

今回はこれまでです
それでは。

暇ですよねwww

そんな時はお金のかからない方法で
遊びを見つけてみよーじゃないですか

自作でプログラム作ればタダです。

ということで迷路を作るプログラムを作ってみました。
解説動画はこちら


今回のプログラムでは穴掘り法というアルゴリズムを使用しています。

どんな感じかというと

1.縦横それぞれ奇数個サイズのマス作る

2.縦横それぞれ奇数座標のマスをランダムに選択する

3.方向をランダムに選択し2マス先のマスを調べて
まだ通路でなければ通路に変える

4.ランダムに選択した方向の2マス先が通路の場合はここで一旦終了

5.新たに縦横奇数座標のマスをランダムに選択し
通路を延ばせるマスが無くなるまで繰り返す

6.スタートとゴールをつけてあげる

ソースコードはこちら
import random
import sys
sys.setrecursionlimit(10**6)

# 穴掘り法で迷路を作る
def make(ny, nx):
    ar = list(range(4)) 
    random.shuffle(ar)
    for i in ar:
        if ny+dy[i][1]<1 or ny+dy[i][1]>=size[0]:
            continue
        if nx+dx[i][1]<1 or nx+dx[i][1]>=size[1]:
            continue
        if maze[ny+dy[i][1]][nx+dx[i][1]]=="□":
            continue
        for j in range(2):
            maze[ny+dy[i][j]][nx+dx[i][j]] = "□"
        make(ny+dy[i][1], nx+dx[i][1])

#height , width    Enter with odd number
size = (101, 51)

maze = [["■"]*size[1] for _ in range(size[0])]
dx,dy = [(1,2), (-1,-2), (0,0), (0,0)],[(0,0), (0,0), (1,2), (-1,-2)]
make(1, 1)
maze[1][1],maze[size[0]-2][size[1]-2] = "す","ご"

for i in maze:
    print(*i)
ちゃんと確認はしてませんが動くはずです。

sizeの所に奇数で縦横のサイズを入れてください。
横はデカすぎるとダメですが、縦は大きくても問題ないです。

こんな感じになるはずです。

スクリーンショット 2020-05-09 15.03.14


黒いのが壁で白抜きが道。
右下にゴールがあります。

でかいサイズで作ったら
PDFとかにでもして
印刷すれば即席巨大迷路の完成です。

どこまで大きなサイズができるかは知りませんが
色々遊べると思うので
動かして遊んでみてくださいませ。

それでは。




このページのトップヘ