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

アルゴリズム


今回は円周率を計算するアルゴリズムを使って
馬鹿でかい円周率を行けるだけ求めてみました

解説動画はこちら





まず最初に問題ですが
円周率の小数点10000桁目の数値は
何になるでしょうか?

こんな問題を解く場合に
馬鹿でかい円周率を求める方法があります。



ガウス=ルジャンドルのアルゴリズム
(Gauss–Legendre algorithm)


円周率を計算する際に用いられる
数学の反復計算アルゴリズム(算術幾何平均法)

円周率計算のアルゴリズムの中でも
トップクラスの収束速度なので、精度が良いらしいです。

アルゴリズムは

スクリーンショット 2024-11-09 16.46.34

となります。


円周率を求める関数のコード

計算精度を設定して、その桁数までを求めていく感じになります。
例えば円周率の小数点100桁まで求めたかったら
計算精度を110-150 とかに設定します。

また反復計算の回数は
log2(x) 以上の回数が必要になるので
計算精度を上げる場合は、反復計算回数も
あわせて上げてください(ここでは20回にしてます)

from decimal import Decimal, getcontext

# 計算精度の設定(小数点以下1万桁以上の精度を確保)
getcontext().prec = 15000

# ガウス=ルジャンドルのアルゴリズムの実装
def gauss_legendre_pi():
    # 初期値設定
    a = Decimal(1)
    b = Decimal(1) / Decimal(2).sqrt()
    t = Decimal(1) / Decimal(4)
    p = Decimal(1)

    # 十分な収束回数で反復計算(math.ceil(math.log2(x))以上)
    for _ in range(20):
        a_next = (a + b) / 2
        b = (a * b).sqrt()
        t -= p * (a - a_next) ** 2
        a = a_next
        p *= 2

    pi_approx = ((a + b) ** 2) / (4 * t)
    return pi_approx

# 円周率の計算
pi_approx = gauss_legendre_pi()
pi_str = str(pi_approx)

# 1万桁目の数値を取得
k = 10000

# 結果を出力
print(f"円周率の小数点以下{k}桁目の数値: {pi_str[k+1]}")
9




まとめ

このPythonコードであれば小数点10万桁くらいまでは余裕です。

それ以上になるとハイスペックな計算資源が必要で
加えてもっと効率の良い高精度計算専用のライブラリを用いたり
並列分散処理などを実装する必要が出てくるかもしれません。

ちなみにこのアルゴリズムでは
2009年に筑波大学の高橋大介という方が
2兆5769億8037万桁
という記録を出しているので
そこから15年以上経った今なら
もっと馬鹿でかい桁まで計算が出来るかもしれません!!

デカいって、いいですよね
男のロマン

どなたか馬鹿でかい円周率の計算
挑戦してみませんか

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

 

今回はハノイの塔を解くプログラムについてです。


解説動画はこちら



ハノイの塔(Tower of Hanoi)とは

古来からあるパズルゲーム



以下のルールに従って
すべての円盤を右端の杭に移動させられれば完成です。

・3本の杭と、大きさの異なる複数の円盤から構成
・初期配置は左端の杭に昇順で積み重ねられている
・1回に1枚ずつ移動させるられるが
 小さいものの上に大きな円盤を乗せることはできない


ハノイの塔は
「再帰的な方法」で解くことができる古典的な問題です。

ディスクの数 n、出発点の棒 source
目的地の棒 target、および補助の棒 auxiliaryとします。

基底条件として、ディスクが1枚だけの場合は
sourceからtargetに直接移動
それ以外の場合は、再帰的に以下のステップを実行:

n-1枚のディスクをsourceからauxiliaryに移動
残りの1枚のディスクをsourceからtargetに移動
n-1枚のディスクをauxiliaryからtargetに移動

配置するディスクの数 n を変更することで
他のケースも同様に解くことができます。



ハノイの塔を解くコード

# ハノイの塔を攻略
def hanoi(n, source, target, auxiliary):
    if n == 1:
        print(f"{n}を{source}から{target}に移動")
        return
    hanoi(n-1, source, auxiliary, target)
    print(f"{n}を{source}から{target}に移動")
    hanoi(n-1, auxiliary, target, source)

n = 3
hanoi(n, 'A', 'C', 'B')
1をAからCに移動
2をAからBに移動
1をCからBに移動
3をAからCに移動
1をBからAに移動
2をBからCに移動
1をAからCに移動



ハノイの塔を描画するコード

widgetsを使ってstepごとの画像を描画しています。
# ハノイの塔を描画する
import ipywidgets as widgets
import matplotlib.pyplot as plt
import numpy as np

data,img_data = [],[]

# ディスクの色リスト
colors = ['red', 'green', 'blue', 'purple', 'yellow', 'silver', 'orange', 'pink']

# 棒の位置
positions = {'A': 0, 'B': 1, 'C': 2}

# ハノイの塔を攻略
def hanoi(n, source, target, auxiliary):
    if n == 1:
        data.append([n,source,target])
        return
    hanoi(n-1, source, auxiliary, target)
    data.append([n,source,target])
    hanoi(n-1, auxiliary, target, source)

# ディスクを描画するための関数
def draw_towers(state, step):
    fig, ax = plt.subplots()
    ax.set_xlim(-1, 3)
    ax.set_ylim(0, 5)
    ax.set_xticks([0, 1, 2])
    ax.set_xticklabels(['A', 'B', 'C'])
    ax.set_yticks([])
    for tower in 'ABC':
        for j, disk in enumerate(state[tower]):
            color = colors[disk - 1]
            width = 0.2 * disk
            height = 0.5
            ax.bar(positions[tower], height, bottom=j * height, width=width, color=color, edgecolor='black')
    ax.set_title(f'Step {step}')
    fig.canvas.draw()
    img_data.append(np.array(fig.canvas.renderer.buffer_rgba()))
    plt.close(fig)

# 初期状態
n = 4  # ディスクの数
hanoi(n, 'A', 'C', 'B')
towers = {'A': list(range(n, 0, -1)), 'B': [], 'C': []}
draw_towers(towers, 0)

# 各ステップごとに状態を描画
for step, (disk, source, target) in enumerate(data):
    towers[target].append(towers[source].pop())
    draw_towers(towers, step+1)

# img_data に保存された画像を表示するためのウィジェット
def show_image(step):
    plt.imshow(img_data[step])
    plt.axis('off')
    plt.show()

slider = widgets.IntSlider(value=0, min=0, max=len(img_data)-1, step=1, description='Step:')
widgets.interactive(show_image, step=slider)
スクリーンショット 2024-08-31 12.59.50


終わりに

インドの巨大な寺院の話が元ネタのようです。

天地創造の時に神が64枚の純金の円盤を重ねて置いた
司祭たちはそこで、昼夜を通して円盤を別の柱に移し替えている
全ての円盤の移し替えが終わったときに
世界は崩壊し終焉を迎える・・・



ハノイの塔のstep数は 2のディスクの枚数乗 -1 なので
2 ** 64 -1 stepかかります。

すっごい回数
おじさん達が移し替えてると思うと
大変だなーと思う今日この頃です。

それでは。


今回はRSA暗号について
考えてみることにしました。


解説動画はこちら


さてさて
RSAッて良く分からないので
Wikiで調べてみることにしました。

RSA暗号とは、桁数が大きい合成数の素因数分解問題が
困難であることを安全性の根拠とした公開鍵暗号の一つ
鍵生成、暗号化、復号の3つのアルゴリズムで定義される

うん・・・・・

RSA暗号は次のような方式である

鍵のペア(公開鍵と秘密鍵)を作成して公開鍵を公開する
まず、適当な正整数 E を選択する

また、大きな2つの素数の組み P,Q を生成し
それらの積(N=PQ)を求めて、組みE,N を
平文の暗号化に使用する鍵(公開鍵)とする

2つの素数P,Qは秘密に保管し
暗号文の復号に使用する鍵(秘密鍵)D の生成にも使用する

暗号化(平文 M から暗号文 C を作成する):
復号(暗号文 C から元の平文 M を得る): 
暗号化(E 乗)は公開鍵 E,N があれば容易に計算でき
復号(D 乗)も容易に計算できる

しかし、秘密鍵 D を知らずに
解読(法 N の下で E 乗根を計算)するのは
「N の素因数を知らないと難しい」と考えられている

つまり
「秘密鍵を用いずに暗号文から平文を得ることは難しい」
と信じられている

これがRSA暗号の安全性の根拠である


はい良くわかりませんです!!!


というわけで
サンプルコードを作ってみました。

# RSA暗号の例
import random
import fractions
import sympy
import warnings 
warnings.filterwarnings('ignore')

# 乱数生成
def _random():
    digit = 10
    return random.randrange(10**(digit - 1),10**digit)

# 最小公倍数
def _lcm(p , q):
    return (p * q) // fractions.gcd(p, q)

# 拡張ユークリッド
def _euclid(x,y):
    c0, c1 = x, y
    a0, a1 = 1, 0
    b0, b1 = 0, 1
    while c1 != 0:
        m = c0 % c1
        q = c0 // c1
        c0, c1 = c1, m
        a0, a1 = a1, (a0 - q * a1)
        b0, b1 = b1, (b0 - q * b1)
    return c0, a0, b0

# キー生成
def generate_key(p = 0,q = 0,e = 0,d = 0,n = 0,l = 0):
    if p == 0:
        while True:
            p = _random()
            if sympy.isprime(p):break
    _p = p
    if q == 0:
        while True:
            q = _random()
            if sympy.isprime(q) and p != q:break
    _q = q
    if n == 0:
        n = p * q
    _n = n
    if l == 0:
        l = _lcm(p - 1, q  - 1)
    _l = l
    if e == 0:
        while True:
            i = random.randint(2,l)
            if fractions.gcd(i, l) == 1:
                e = i
                break
    _e = e
    if d == 0:
        _c, a, _b = _euclid(e, l)
        d = a % l
    _d = d
    return _e,_d,_n,_p,_q,_l

# 暗号化
def encrypt(_e,_n,plaintext):
    st,ciphertext = "",[]
    for i in map((lambda x: pow(ord(x), _e,_n)),list(plaintext)):
        ciphertext.append(i)
        st += str(i)
    return st,ciphertext

# 復号化
def decrypt(_d,_n,ciphertext):
    cip , st = [] , ""
    for i in  list(ciphertext):
        tmp = chr(pow(i, _d,_n))
        cip.append(tmp)
        st += str(tmp)
    return st

使う際は平文として暗号化したい
文字列を定義しておきます。

plain_text = '文字列を入力'


早速使っていきましょう。
暗号化するところと
復号化するところのコードです。
# 初期化
ciphertext = []
_e = _d = _n = _p = _q = _l = 0

# キー生成
_e,_d,_n,_p,_q,_l = generate_key(0,0,65537)

# 暗号化
e_text , ciphertext = encrypt(_e,_n,plain_text)

# 復号化
d_text = decrypt(_d,_n,ciphertext)

print('素数 p : {0}'.format(_p))
print('素数 q : {0}'.format(_q))
print('l : {0}'.format(_l))
print('公開鍵 e : {0}'.format(_e))
print('公開鍵 n : {0}'.format(_n))
print('秘密鍵 d : {0}'.format(_d))
print()

print('暗号文(String) : {0}'.format(e_text))
print()
print('暗号文(List) : {0}'.format(ciphertext))

素数 p : 9558530557
素数 q : 5377612577
l : 12850518531506468064
公開鍵 e : 65537
公開鍵 n : 51402074140962015389
秘密鍵 d : 104902992421928993

暗号文(String) : 502447033995121916661752375033108949410513472439923007515854175237503310894941051347243992300751585434094647152221189902182494137669912247793511588278946816434351158827894681643422454674385942603378

暗号文(List) : [50244703399512191666, 17523750331089494105, 13472439923007515854, 17523750331089494105, 13472439923007515854, 34094647152221189902, 18249413766991224779, 3511588278946816434, 3511588278946816434, 22454674385942603378]

素数P,Qや
鍵N,Dは毎回変わってしまいます。

公開鍵Nと秘密鍵Dがあれば
暗号を解読することができます。

暇な方は上記の鍵と暗号文から
復号してみてくださいね。

人類を平和にする
「あいことば」を暗号にしてみましたよ

解読面倒な方は
動画見て下さいねーー


このサンプルコードでは
およそ20桁ほどの秘密鍵になりますんで
9千京回ほどブルートフォースすれば
暗号解読できるかもしれません!!!!

鼻血出せば解けるかもしれませんねえ
そんなアニメが有るとか無いとか

自分ならやらないっすねーー

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



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

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

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

解説動画はこちら



さて素数ですが知らない人は
ほとんどいないと思いますが
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とかにでもして
印刷すれば即席巨大迷路の完成です。

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

それでは。




このページのトップヘ