知ってるよ, って方は飛ばしてください.
次の式で表される数列のこと.
を定義しない場合もありますが, 今回はプログラムで扱うので便宜上 から開始するものとする.
から は次の通り:
i : 0, 1, 2, 3, 4, 5, 6, 7
F : 0, 1, 1, 2, 3, 5, 8, 13
フィボナッチ数列の第項を計算する関数 fib(n)
を定義します.
以下のコードブロックで実行してみる.
普通に結果も出て一件落着とはなりません. 実は, この実装では無駄な計算をたくさんしています.
fib(5)
を例に計算を追跡します.
fib(5) = fib(4) + fib(3) ... (1)
= fib(3) + fib(2) + fib(3) ... (2)
= fib(2) + fib(1) + fib(2) + fib(3) ... (3)
= fib(1) + fib(0) + fib(1) + fib(2) + fib(3) ... (4)
= fib(1) + fib(0) + fib(1) + fib(1) + fib(0) + fib(3) ... (5)
= fib(1) + fib(0) + fib(1) + fib(1) + fib(0) + fib(2) + fib(1) ... (6)
= fib(1) + fib(0) + fib(1) + fib(1) + fib(0) + fib(1) + fib(0) + fib(1) ... (7)
= 1 + 0 + 1 + 1 + 0 + 1 + 0 + 1 = 5
計算を見てわかるように, fib(0), fib(1)
になるまでひたすら展開しています.
例えば (2) の時点で fib(3) + fib(2) + fib(3) = fib(3) * 2 + fib(2)
的なことができれば, (5) から (7) における 2回目の fib(3) を展開する必要がなくなります.
fib(n)
が呼ばれた回数をv[n]
に記録する配列v
を用意し, fib(5)
の場合で確認する.
すごく雑に見積もります.
愚直な実装では, fib(n) = fib(n - 1) + fib(n - 2)
という風に, 各 の計算が2つに分裂します.
そして は になるまで ずつ減少していくので, 倍, 倍, 倍, ..., を 回, 即ち です.
ここで一度, 自分の手や頭でfib(5)
を計算してみる.
恐らくこんな感じで計算する.
fib(0) = 0, fib(1) = 1
fib(2) = fib(1) + fib(0) = 1 + 0 = 1
fib(3) = fib(2) + fib(1) = 1 + 1 = 2
fib(4) = fib(3) + fib(2) = 2 + 1 = 3
fib(5) = fib(4) + fib(3) = 3 + 2 = 5
あれ? 僕賢い? でも明らかに の線形時間()で解けてるからコンピュータより賢い?
自惚れるんじゃあない. 何が違うんだろうかと考える.
fib(n) = 0, 1
という数値に落ち着く. 逆に言えば, の間はひたすら展開する.というわけで, メモ化です. 実装しましょう.
色々な実装方法がありますが, ここではメモを引数として与えてやります.
また, メモの初期化方法も様々です.
ここでは, メモされていないことを None
として扱うため, None
で初期化します.
未メモ状態の表し方として,
-1
(フィボナッチ数列の定義から, 負の値をとることがない)bool
型配列を別に用意などの方法もあります.
以下で実行してみる.
愚直に計算した場合と同様に, 呼び出し回数(メモを用いず実際にfib(n)
を計算した回数)を調べます.
それぞれの呼び出し回数を比べる(表を使ってみた).
fib(n) | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
愚直 | 3 | 5 | 3 | 2 | 1 | 1 |
メモ | 0 | 0 | 1 | 1 | 1 | 1 |
メモ化した場合は なので当然と言えば当然. 表にするまでもない.
の時点でこの差なので, をより大きい値にすると差がもっと大きくなることが容易に想像がつく. Magicode のコードブロック上で を大きくすることは負担になる可能性があるので, 控えましょう(どのくらいまでやってくれるのかは気になるが).