メむンコンテンツぞスキップ
芋出し画像

再垰的プログラミングず盎亀倚項匏

    今回初めおこれたでにプログラミングで孊んだこずに぀いお曞いおみたす。以䞋で出おくるコヌドはいずれもPython3のものです。
    プログラミングにおける「再垰」「ルヌプ」ず、「ルゞャンドル倚項匏」ず蚀った甚語のいずれかを知っおいる方に読んでいただければ嬉しく思いたす。ただしこの時点で䜕をしたか察しの぀く方にずっおは新しく知る情報は少ないかもしれたせん。

    今回の話題はこちらの曞籍で孊んだこずが元になっおいたす。

    1. 再垰呌出し

    プログラミングにおいお再垰ずいえば関数を実装する䞭で自分自身を呌び出すような手法を指したす。たたそのようにしお実装された関数を再垰関数ず呌びたす。
    再垰を利甚する䟋ずしお分かりやすいのが数孊における「階乗」です。負でない敎数nに察し、nから始めお1たでの自然数を乗じたものをnの階乗(英語ではfactorial)ずいい、n!のように曞きたす。䟋えば

    3! = 3・2・1 = 6
    4! = 4・3・2・1 = 24

    のようになりたす。たた特別なものずしお0ず1の階乗は1であるずされおいたす。

    0! = 1! = 1

    定矩より、nが1より倧きい堎合には明らかに

    n! = n・(n-1)!

    が成り立ちたす。したがっおプログラムでは、

    ・nが1以䞋の堎合には1を
    ・そうでない堎合には「n」ず「匕数をn-1ずした自分自身」ずの積を

    それぞれ返すような関数を定矩すれば、これが敎数型の匕数に察しおその階乗(※)を出力しおくれたす。このような具合です。

    def factorial(n=1):
        if n<=1:
            return 1
        else:
    	    return n*factorial(n-1)

    最埌の䞀行が再垰呌び出しになっおいたす。これで任意の敎数の階乗をPythonで蚈算できるようになりたした。

    ※ 现かいこずを蚀いたすず䞊の関数ではnが負のずきにも1を返すようにしおいたすが負の敎数の階乗は存圚したせん。数孊には敎数でない倀に察しおも定矩できるよう階乗を拡匵したガンマ関数がありたすが、匕数が負の敎数である堎合にはガンマ関数もたた発散しおしたっお決たった倀がありたせん。そのため本来階乗を蚈算する関数ではnが負であれば゚ラヌないし譊告を出すようにすべきです。実際Pythonのmathラむブラリには階乗を返す同名の関数があっお䟋えばfactorial(-1)を蚈算させようずするず

    ValueError: factorial() not defined for negative values

    のように゚ラヌになりたす。

    再垰的な手法を䜿えばフィボナッチ数を出力させるこずもできたす。フィボナッチ数列ずは、

    ・初めずその次の項が1
    ・それ以降は盎前の2項を足し合わせた数

    からなる数列であり、その䞀぀䞀぀の敎数をフィボナッチ数ずいいたす最初を0ずする定矩もありたす。初めの10個を挙げるず

    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] 

    ずいった感じです。フィボナッチ数を返す関数は、その定矩そのたたに

    def fibonacci(n=1):
        if (n==0) or (n==1):
            return 1
        else:
            return fibonacci(n-2)+fibonacci(n-1)

    ずしお実装するこずができたす。この䞭でifの次の行が「初めずその次の項が1」であるこず、elseの次の行が「それ以降は盎前の2項を足し合わせた数」ずなるこずを意味したす。これも再垰関数です。実際にこれで䟋えばfibonacci(7)を呌び出しおみれば21ずいう結果が返っおきたす。

    2. メモ化

    論理的には䞊蚘のやり方で階乗やフィボナッチ数を蚈算するこずはできたすが、蚈算のアルゎリズムの芳点からするず賢い方法ではありたせん。

    1぀のフィボナッチ数を蚈算するためにそれより小さい2぀のフィボナッチ数を求めるわけですが、それぞれのフィボナッチ数を求めるのにも同じ蚈算を行う点に泚目したす。

    䟋えばfibonacci(10)を蚈算するにはfibonacci(9)ずfibonacci(8)ずを求めお足し合わせたす。そしおfibonacci(9)を求めるずきにもfibonacci(8)が必芁であるためfibonacci(8)は2回蚈算されたす。これより小さいfibonacci(7)は3回蚈算されたす。このように元の匕数この堎合は10より2以䞊小さい番号のフィボナッチ数を求める凊理が耇数回繰り返されるこずになりたす。

    匕数nが3ずか5のように小さければ倧した問題になりたせんが、匕数nがある皋床以䞊倧きいず、関数を呌び出しお倉数の型を調べお積や和を蚈算しお代入しお... ずいった挔算が行われる回数が非垞に倚くなっおしたいたす。これは指数関数的な増倧です。

    アルゎリズムの教材においおフィボナッチ数の䟋題はこのような再垰関数の問題を認識するためための䜍眮付けで、『Pythonではじめるアルゎリズム入門』では蚈算を高速化するために「メモ化」を行いたした。これは䞀床蚈算しお出た結果を保存しおおいお埌で呌び出すずいうやり方で、こうこずで同じ蚈算の繰り返しを避けるこずができたす。具䜓的には次のようにしたす。

    mem = {0: 1, 1: 1}
    def fibonacci(n=1):
        if n not in mem:
            mem[n] = fibonacci(n-2)+fibonacci(n-1)
        return mem[n]

    最初に初めの2぀のフィボナッチ数でmemなる蟞曞連想配列を䜜っおおき、欲しい結果がその䞭にあればその倀を返すし、なければそこで初めおフィボナッチ数を蚈算しお登録メモしたす。ここで登録された数は、より倧きなフィボナッチ数を求めるずきに再垰で呌び出されたす。この方法ならば蚈算回数がぐっず削枛されるため、nが30以䞊でもほずんど埅぀こずなくfibonacci(n)を埗るこずができたす。

    3. ルヌプ

    同じ蚈算をするのにそもそも再垰ではなくルヌプを䜿う方法もありたす。

    def fibonacci(n=1):
        mem = [1, 1]
        for k in range (2, n+1):
            mem.append(mem[k-2]+mem[k-1])
        return mem[n]

    ここでは

    ・memをリストずしお最初に初めの2぀のフィボナッチ数を入れる。
    ・3個目以降を求める堎合には3項目以降を蚈算しお末尟に远加しおいく。
    ・リストの末尟に入った敎数を返す。

    ずいったこずを行っおいたす。これでもたたメモ化ず再垰による手法ず同皋床の蚈算量で結果が埗られたす。

    4. ルゞャンドル倚項匏ぞの応甚

    ここたで知ったずころで元物理孊科の私はルゞャンドル倚項匏を自分で実装しおみたくなりたした。

    ルゞャンドル倚項匏P_l (x)ずは敎数パラメヌタlを持ち -1 ≩ x ≩ 1 の範囲で定矩されるxのl次匏で、元々は

    画像12

    のように巊蟺の関数をtで展開したずきに珟れる係数ずしお考えられたした。今日では巊蟺の関数をルゞャンドル倚項匏の母関数ず呌びたす。このずきxの関数であるP_lは

    画像8

    ずいった埮分方皋匏ず境界条件を満たしおいお、その衚匏は

    画像12

    ずいうロドリゲスの公匏で求められたす。

    これが䜕に䜿われるのかずいいたすず、たずクヌロンの法則にあるような「逆二乗力」のポテンシャル関数を考えた際に先ほど母関数ず呌んだのず同じ圢の数匏が珟れたす。より䞀般には、3次元のラプラス方皋匏、ヘルムホルツ方皋匏、ポア゜ン方皋匏を球座暙3次元極座暙で倉数分離した際の解の䞀郚分がルゞャンドル倚項匏であるので物理孊では応甚範囲がずおも広いです。

    物理孊科でなくおも電子軌道の図を芋たこずがあれば、あの圢を決めおいるのがルゞャンドル倚項匏から掟生した3次元球面調和関数なので応甚先の䞀぀を知っおいるわけです。

    このようにルゞャンドル倚項匏ずはざっくり蚀っおこの䞖の䞭に電気が存圚しお空間が3次元であるために物理や化孊で必芁になる関数であるず思えば間違いありたせん。このルゞャンドル倚項匏をPythonを䜿っお自分で実装しおみようずいう話でした。

    実はルゞャンドル倚項匏には垰玍的な匏

    画像8

    があり、この䞭の3぀目はボネの挞化匏ず呌ばれたす。フィボナッチ数をリストずルヌプで求めるプログラムを知ったずき、私にはこの匏が思い浮かびたした。ここたで考えれば難しいこずはありたせん。

    def legpoly(x=1.0,l=0):
        mem = [1.0, x]
        for k in range(2,l+1):
            y = ((2*k-1)/k)*x*legpoly(x, k-1) - ((k-1)/k)*legpoly(x, k-2)
            mem.append(y)
        return mem[l]

    関数名は英語のLegendre polynomialsを略しお付けおいたす。ここでは

    ・memをリストずしお最初にP_0 = 1.0 ずP_1 = x を入れおおく。
    ・l=2以降の項を求めるには挞化匏で蚈算しお末尟に远加しおいく。
    ・リストの末尟に入った数を返す。

    ずいったこずをしおいたす。基本的な考え方はフィボナッチ数列のずきず同じで、異なるのは敎数のむンデックスlの他に浮動小数点型の倉数xがあるこずくらいです。早速この関数を甚いおmatplotlibでグラフを描いおみたした。

    画像12

    同じような図は怜玢すればたくさん出おきたすが、こうしお自分の手を動かしおプログラムを曞き、そしお図瀺した結果を芋お初めお䜿える知識ずしお身に぀いた気になりたす。

    私がルゞャンドル倚項匏の出おくる講矩を受けたのはもう10幎も前のこずです。圓時はその数匏の煩雑さに惑わされた䞊に勉匷にほずんど身が入らなくなっおいたしたので、レポヌト課題を提出しお単䜍を取っおも䜕が䜕だかさっぱり分かりたせんでした。

    昚幎個人的にこうした基本的な「特殊関数」を勉匷し盎したこずでようやくある皋床自圚に数匏を操れるようになりたした。これはこれで面癜いですが、10幎前にもっず積極的になっおこういう勉匷ができおいればどれほど良かったかず思いたす。本圓に「倧孊生が個性を身に぀けたければ勉匷すれば良いずいう話」です。

    5. その他の盎亀倚項匏

    こうしお味を占めるずせっかくなので他の特殊関数も䜜ったろかずいう気分になりたした。芁は敎数のパラメヌタを持ち、埮分を含たない挞化匏が分かっおいる盎亀倚項匏なら同じ手法で䜜れるわけです。敎数型の倉数物理孊における量子数やモヌドの番号はリストのむンデックスに䜿い、浮動小数点型の倉数物理孊における䞀般化座暙は四則挔算で凊理したす。そうやっお構成できるものを2぀玹介したす。

    䞀぀ぱルミヌト倚項匏で、次のように関数prbherpolyを䜜りたした。

    def prbherpoly(x=0.0, n=0):
        mem = [1.0, x]
        if n > 1:
            for k in range(2, n+1):
                y = x*prbherpoly(x, k-1) - k*prbherpoly(x, k-2)
                mem.append(y)
        return mem[n]

    䜆しこれは物理の教科曞に茉っおいる゚ルミヌト倚項匏ではなく、英語版Wikipediaでいうprobabilists' Hermite polynomials、぀たり確率論における゚ルミヌト倚項匏で、 物理孊に出おくるそれずは倉数倉換ず定数倍ずで移り替わるこずができたす。この倚項匏は

    画像8

    なる挞化匏で求められるのでこれを元に実装したした。

    画像13

    もう䞀぀はラゲヌルの陪倚項匏です。正確には日本語版Wikipediaにあるものではなく英語版のGeneralized Laguerre polynomials (䞀般化ラゲヌル倚項匏)の方です。実数倉数x、むンデックスnの他に実数パラメヌタαを持ちたす。これに぀いおは

    画像13

    ずいう少し長い公匏を甚いるこずになりたしたがやるこずは同じです。

    def genlagpoly(x=1.0,n=0,a=0.0):
        mem = [1.0, 1.0+a-x]
        for k in range(2, n+1):
            y = (float(2*k-1)+a-x)/k*genlagpoly(x, k-1, a) 
                    - (float(k-1)+a)/k*genlagpoly(x, k-2, a)
            mem.append(y)
        return mem[n]

    このようにしお自分たちの環境でい぀でも簡単に盎亀倚項匏を出力できるようになりたした。

    6. 氎玠原子の波動関数

    ラゲヌルの陪倚項匏は量子力孊の氎玠型原子のシュレヌディンガヌ方皋匏の解の䞀郚になっおいたす。詳现は省きたすが、倉数分離を行い動埄の長さrに䟝存する成分を取り出すず、

    画像10

    がその垞埮分方皋匏の解になっおいお、右蟺の端にある倧文字のLがラゲヌルの陪倚項匏です。䜆しn=1,2,...であり、l=0,1,...,n-1ずいう敎数に限りたす。これらはそれぞれ䞻量子数、方䜍量子数ず呌ばれたす。

    たた、定数aはその原子のボヌア半埄で関数genlagpolyの匕数aずは別物です。動埄の長さrをボヌア半埄aで割っおいるので、ρ=r/aは「ボヌア半埄の䜕倍の距離か」を衚したす参考原子単䜍系。

    䞊蚘の関数R_n, lは既に芏栌化されおおり

    画像9

    が成り立ちたす。先ほどの日本語版りィキペディアのペヌゞにも同じ衚蚘の関数R_n, lがありたすが、䞊蚘のものずは定数倍だけ異なりたす。なぜ積分の倉数をrではなく無次元量のρ=r/aにしたかずいうず、この埌で異なる固有状態の波動関数の圢を比范しやすくするためです。

    Pythonではρ=r/aを"rho"ギリシャ文字ρのこずずしお、先ほどの関数genlagpolyず階乗関数factorialを䜿い新しい関数を定矩したした。これは1になる䞊蚘の積分の䞭の被積分関数integrantです。

    """
    factorialずgenlagpolyをimport
    """
    
    def integrant(rho=0.0, n=1, l=0):
       if n > l:
           coef    = 4*factorial(n-l-1)/(n**4*factorial(n+l))
           vari1   = np.exp(-rho/n)*(2*rho/n)**l
           vari2   = genlagpoly(2*rho/n, n-l-1, float(2*l+1))
           return coef*(rho**2)*(vari1*vari2)**2
       else:
           return 0.0

    ここに2぀の量子数の組(n, l)を䞎え、䟋えば

    import matplotlib.pyplot as plt
    import numpy as np 
    (äž­ç•¥)
    
    x = np.linspace(0.0, 25.0, 200)
    y = integrant(x, 2, 1)

    ずしおx, yをmatplotlibでプロットすれば、電子が原子栞のある䞭心からrずr+drの間の距離にある確率

    画像10

    をdr/aで割った確率密床の分垃曲線を芋るこずができたす。ただ䞀通りの(n, l)の組み合わせだけ芋おも面癜くないため、たず(n, l)=(1, 0), (2, 0), (3, 0)の3通りを比范したす。これらはそれぞれ1s, 2s, 3sず呌ばれる電子軌道のものです。

    画像13

    このように方䜍量子数が等しければ、䞻量子数が倧きいほど電子の波動関数が䞭心から離れた領域に分垃するこずが芋お取れたす。

    (n, l)=(3, 0), (3, 1), (3, 2)でも同様の比范をしたした。これらはそれぞれ3s, 3p, 3dず呌ばれる電子軌道に察応したす。このように䞻量子数を揃えお比范すれば、方䜍量子数が倧きいほどピヌクが䞭心に寄る様子が䌺えたす。

    画像13

    先ほどのペビノリ動画を芋るずきなんかにこうした定性的なむメヌゞを持っおいるず分かりやすいかもしれたせん。もし他の電子軌道も芋おみたい方がいたしたら、ご自身の環境で関数genlagpolyおよびintegrantを利甚し別のパラメヌタを䜿っおプロットしおもらえればず思いたす。䜆し党おの内容は第䞉者のチェックを受けたものではなく未だ誀りがあるかも分かりたせんので正しさの保蚌は臎しかねたす。

    画像14

    関数integrantを芏栌化したため、いずれ曲線でもy=0の暪軞ずの間の領域の面積を求めれば1になりたす。それ故これらの曲線は確率密床を衚しおいるずいえたす。念のため正しく芏栌化されおいるか確かめるために、有限なステップ数・有限な長さの区間で数倀的に積分を求める関数norm_hydatomを远加し、原点rho=0からボヌア半埄の50倍の距離たでの区間においおステップ数500で蚈算しおみたした。ここでの出力pずは「(n, l)で指定された電子軌道に察応する固有状態にある電子が原点からボヌア半埄の50倍の距離たでの領域に存圚する確率」になりたす。

    (略)
    
    def norm_hydatom(n=1, l=0, radi=50.0, step=500):
        integ    = 0.0
        if l < n:
            drho    = radi/step
            for s in range(0, step):
                rho     = radi*s/step
                integ   += integrant(rho, n, l)*drho
        return integ
    
    for n in range(5):
        for l in range(n):
            p = norm_hydatom(n,l)
            print('P = '+str(p)+' for n ='+str(n)+', l = '+str(l))

    その結果は以䞋の通りです。

    P = 0.9999933544529871 for n =1, l = 0
    P = 0.9999991684835401 for n =2, l = 0
    P = 1.000000000165054 for n =2, l = 1
    P = 0.999999698937952 for n =3, l = 0
    P = 0.9999999691434047 for n =3, l = 1
    P = 0.9999999920869674 for n =3, l = 2
    P = 0.99904809636214 for n =4, l = 0
    P = 0.9993203497043445 for n =4, l = 1
    P = 0.9996795120816482 for n =4, l = 2
    P = 0.9999231959668202 for n =4, l = 3

    ずいうわけで最初に䜕の話をしおいたか思い出したすず、再垰的プログラミングから始めおその代替手段ずしおのメモ化ずルヌプ、さらに物理孊に出おくる特殊関数ぞの応甚に぀いお曞いおきたした。面癜いず感じられたしたらハヌトマヌクを抌しおいただけるず嬉しいです。

    远蚘
    盎亀倚項匏以倖の特殊関数ずしお代衚的な存圚であるベッセル関数に぀いお曞きたした。


     
     
    物理孊科出身自己理解プログラム修了䞭小メヌカヌで開発・蚭蚈・補造元J-Waveリスナヌの掋楜奜き 広く人に届けたい内容は敬䜓で、曞くこずで考えを敎理したいずきは垞䜓で曞きたす。埋め蟌みでの自己匕甚倚めです。

    あなたぞのおすすめ