結城浩です。いつもご愛読ありがとうございます。
書籍化第一弾として『数学ガールの秘密ノート/式とグラフ』が 2013年7月に刊行されます。ぜひ応援してくださいね!
なお、書籍化第二弾は2013年12月刊行の予定です。
フィボナッチ数列を使って、カジノで賭ける方法
次にフィボナッチ数列法とダランベール法を併用してゲームを進めていく方法を紹介します。最初はダランベール法を使って賭け、その後フィボナッチ数列法に切り替えます。ダランベール法から切り替える時のポイントは、「ダランベール法でプレイし、当たりが10回来た時」です。この場合、次も勝つという確率が低いことを見越してフィボナッチ数列法にシフトします。この応用法の場合、10回負けた時には適応できないので注意してくださいね。逆に、フィボナッチ数列法を終了するタイミングなのですが、実ははっきりと決まっていません。負けた時でもよいですし、ゲームを終了する時でも構いません。
【勝った場合】
一番右と左端(この場合1と3)を消去し、真ん中の数字が2だけになりました。真ん中の数字が1つだけになったらまた最初に戻り、1+3 = 4を賭けます。
【負けた場合】
負けた数字を書き足します。1・2・3・4となり、再び一番右と左端を足した金額を賭けます。この場合は1+4 フィボナッチ数 = 5ということになります。
フィボナッチ数列法のまとめ
フィボナッチ数列法は、資金をできるだけ長持ちさせることができます。資金が少ない時、カジノでゆっくりプレイしたい時などに、ちょうどよいベッティングシステムと言えるのではないでしょうか?どこでゲームを終了させるか…最終的な判断はあなたが下さなければなりません。フィボナッチ数列法を上手に利用し、ぜひカジノで利益を得てくださいね。
なお、ルーレットやバカラのように、「偶数か奇数」「勝ちか負け」など2択(イーブンベット)のゲームを楽しみやすいのは、ライブバカラです。フィボナッチ数列を使った賭け方を試してみたい方は、当サイトで紹介しているライブカジノで遊べるカジノからお好みのカジノを選んでみてください。
それぞれの方法の演算速度
再帰法が遅い理由は端的に言うと「演算回数が多いから」です。この図をみてください。
これは再帰法による Fib(5) の演算の概念図です。Fib(n)のnが大きくなると爆発的な勢いで演算回数が多くなります。
方法 | 演算回数の特徴 |
---|---|
一般項を用いる解法 | (内容は複雑だが)どんなnに対しても1回 |
反復的な解法 | nに応じて演算回数が線形的に増加する |
再帰的な解法 | nに応じて演算回数が指数関数的に増加する |
この特徴から、反復法より一般項の方が速いんじゃないかと思う人がいると思いますが、一般項の計算に用いている方法は全く最適化していないので反復法より遅いです( Fib(10000) でも試してみましたが、それでも反復法の方が5倍くらい速いです)。誰か最適化の方法を知っている人は教えてください。
さて、反復法は速く、再帰法は遅いというのが分かりました。でも再帰法は「あること」をすることで反復法並み、もしくはそれ以上の速さを手に入れることができます。それはメモ化です。
メモ化の理念
一度呼び出した関数を再び使う時に再演算しなくて済むように、一度呼び出した関数の戻り値をメモに保存しておいて、2回目以降呼び出されたときはメモに書かれた値を使う、というのがメモ化の理念です。
例えば、先ほど示した概念図をみて分かる通り Fib(4) は4回、 Fib(5) は8回関数を呼び出します。そのため、メモ化無しだと フィボナッチ数 Fib(6) を計算するのに12回関数を呼び出す必要があります。しかし、 Fib(5) はすでに求めた Fib(4) をわざわざもう一回求めなおしています。この Fib(4) の戻り値をメモに保存して以降はそれを使うようにすると、 Fib(6) は9回の呼び出しで済みます( Fib(3) もメモ化することで8回となります)。
第36回 いとしのフィボナッチ(後編)
【書籍刊行のお知らせ】
結城浩です。いつもご愛読ありがとうございます。
書籍化第一弾として『数学ガールの秘密ノート/式とグラフ』が 2013年7月に刊行されます。ぜひ応援してくださいね!
なお、書籍化第二弾は2013年12月刊行の予定です。
一つずらした自分になる
僕 「だから、フィボナッチ数列というのはこういう数列になる。この数列を研究してみよう」
ユーリ 「うん」
僕 「数列の研究ではまず — —」
ユーリ 「《階差数列を求める》んでしょ! ユーリ、やってみる!」
フィボナッチ数列の階差数列を求める
ユーリ 「へえっ! おもしろい! フィボナッチ数列は — —階差数列を求めると、一つずらした自分になるんだね!」
フィボナッチ数列の階差数列は、一つずらした自分になる
僕 「確かにおもしろいな、その発見」
ユーリ 「だよね! ……あれ? でもこれはあたりまえかにゃ?」
僕 「あたりまえというと?」
フィボナッチ数 ユーリ 「だってさ、フィボナッチ数列って、二つ足したら次になるんでしょ? だったら、差をとったら一つずらした自分になるのはあたりまえじゃん」
僕 「まあ、あたりまえといえばあたりまえなんだけどね。簡単な式変形でわかるよ」
ユーリ 「これでなんで《わかるよ》って言えんの?」
僕 「だって、ほら、左辺の $F_ - F_$ という式は添字の部分をよく見ると、隣り合っている二つの項の差を取っていることがわかるよね。つまり階差を求めているわけだ」
ユーリ 「ほー」
僕 「そして右辺の $F_n$ という式はフィボナッチ数列の一般項、つまり第 $n$ 項だよね。だから、この式 $F_ - F_ = フィボナッチ数 F_n$ は《階差を取ると自分になる》ということを表現している」
ユーリ 「してないよ」
僕 「え?」
ユーリ 「《階差を取ると自分になる》じゃなくて、《階差を取ると一つずらした自分になる》でしょ? だって、ほんとの階差なら $F_ - F_$ じゃなくて、 $F_ - F_n$ のはずだもん」
僕 「あ、そ、そうだね。その通りだ」
ユーリ 「階差が自分になったら、 $2$ のべきじょうになっちゃうし」
僕 「ユーリはよくそういうのを見つけるよね」
( ユーリ はめんどうくさがりだけれど、妙なところできっちりミスを指摘するんだよな……)
ユーリ 「ねーお兄ちゃん。そんなことより、気になることあんだけど」
僕 フィボナッチ数 「なに?」
ユーリ 「ユーリがね、一つずらした自分になるって言ったときにね、お兄ちゃん、すぐに数式を出してきたじゃん?」
僕 「ん? まあ、そうだね」
ユーリ 「あれはなんで?」
僕 「なんでと言われても困るけど……」
ユーリ 「あのね、なんでお兄ちゃんはすぐに数式を出したの? 出そうと思ったの?」
僕 「それは……きちんと答えるのは難しいな。まず、数列について何か確かなことを言おうとしたら、 たいていの場合は、数式を使うしかないからだよ。 《フィボナッチ数列の階差数列は一つずらした自分》を確かめるために、 フィボナッチ数列の定義の式を持ち出してきたんだ」
ユーリ 「……」
僕 「ねえユーリ。お兄ちゃんはね、数学をするとき、具体例を作って考え、数式を使って確かめるのが好きだ。 学校の勉強のときもそうだし、自分で好きな数学の本を読むときもそうだよ」
ユーリ 「具体例を作って考え、数式を使って確かめる……」
フィボナッチ数 僕 「そう。だから、なんていうのかな — —数学で数式を使うのは《いつもやってること》なんだよ。だからユーリがフィボナッチ数列について見つけた発見も、 《数式を使って確かめよう》とすぐ思った。それは、いつもやってること、あたりまえのことなんだ」
ユーリ 「ふーん……」
僕 「水を飲むのに蛇口をひねるとか、ご飯を食べるのにお箸を持つとか、数学で数式を使うっていうのはそのくらい自然なことかもしれないよ。 もちろん場合によっては手で水をすくって飲むことも、おにぎりを手で食べるということもあるけれどね」
ユーリ 「へー……」
いつも大きく?
僕 「それにしても、フィボナッチ数列の階差数列を取ると一つずらした自分になるっていう《ユーリの発見》は、とてもおもしろい発見だと思うよ」
ユーリ 「だよね。ところでさ、お兄ちゃん」
僕 「なに?」
ユーリ 「等差数列とか、等比数列とか、フィボナッチ数列とかいろいろ教えてくれたけど、いつも大きくなるばっかりじゃん? 他の数列を考えてもいーよね」
僕 「等差数列がいつも大きくなるとは限らないよ。この数列は等差数列だけど、だんだん小さくなってる」
$$ 100, 99, 98, 97, 96, 95, \ldots $$
ユーリ 「あ、そっか。この数列は $0$ で終わるの?」
僕 「いやいや、そこから先はマイナスに突入する」
$$ 100, 99, 98, 97, 96, 95, \ldots, 2, 1, 0, -1, -2, -3, \ldots $$
ユーリ 「あー、そりゃそっか」
僕 「小さくなる等差数列は 公差 ( こうさ ) がマイナスだってことだね。それは階差数列を調べてみれば一目瞭然だ」
ユーリ 「ふむふむ」
僕 「等比数列でも小さくできる。公比を $1$ より小さな正の数にすればいい。たとえば $\frac$ とかね」
ユーリ 「あ、そっか。それで小さくなるか。公比がマイナスでも小さくなっていくよね」
僕 「いやいやだめだよ。公比がマイナスなら、小さくなったり大きくなったりする」
ユーリ 「なんで……あそっか!』
僕 「公比がマイナスだと、かけるたびに正の数と負の数が交互に反転するからね」
ユーリ 「そーか、そーだね……ねーお兄ちゃん。もっと変な風に大きくなったり小さくなったりする数列作ってよ!」
株式会社 イーブ

個人情報保護 セキュリティ保護 お問い合わせ
EeBlog(テクニカルブログ)
フィボナッチ数
n番目のフィボナッチ数を F(n) として
1.F(0) = 0
2.F(1) = 1
3.F(n) = F(n – 1) + F(n – 2)
また、フィボナッチ数の性質として2の連続するフィボナッチ数の比は n が無限に増加するに従って黄金比に漸近します。
アルゴリズム①
ではまず、n 番目のフィボナッチ数を求めるアルゴリズムを、フィボナッチ数の定義からそのまま記述します。
フィボナッチ数はすぐに大きくなるので BigInteger を使用して桁溢れしないようにしています。
これでフィボナッチ数を求めることができるのですが、このアルゴリズムは非常に遅いです。n = 40 ぐらいでもうかなり遅いはずです。 n = 50 で呼び出せばしばらくは戻ってこないでしょう。
このアルゴリズムが遅い理由はメソッドのなかで自分自身を呼び出す再帰呼び出しが2回行われているからです。このためこのアルゴリズムはフィボナッチ数を求めるのに O(PHI^n)、(PHI は黄金比、およそ 1.618) の時間がかかります。
アルゴリズム②
フィボナッチ数列は n 番目のフィボナッチ数を求めるためには n – 1 番目と n – 2 番目のフィボナッチ数が必要ですが、逆に言うと 0 から n 番目までのフィボナッチ数が求まっているときに n + 1 番目のフィボナッチ数を求めるには、 n 番目と n – 1 番目のフィボナッチ数を加算すれば良いわけです。つまりフィボナッチ数を 0 番目から順番に求めていき、このときに2つ前と2つ前のフィボナッチ数を覚えておけばいいのです。
これでフィボナッチ数を O(n) で求めることができるようになりました。
さきほどのアルゴリズムでは n = 50 フィボナッチ数 のフィボナッチ数を求めるには途方もなく時間がかりますが、このアルゴリズムでは一瞬です。おそらく n = 10000 でもまだ余裕です。n = 100000 くらいになると少し遅くなってくるのではないでしょうか。
アルゴリズム③
アルゴリズム②では1つ前と2つ前のフィボナッチ数を覚えておいて O(n) で計算できるようにしましたが、このようなアルゴリズムの工夫をしなくてもメモ化 (memoization、memorization ではない) というテクニックを使うと、二重再帰呼び出しがあっても O(n) で計算できるようになります。
これは一度求めた値をすべて保存しておき、2度目からは保存しておいた値を返すようにしています。これによってある引数の値での1度目の呼び出しでは O(n) ですが、同じ引数の値での 2度目以降の呼び出しは O(1) になります。
アルゴリズム④
フィボナッチ数を求めるのに O(PHI^n) から O(n) になってずいぶん速くなりましたが、アルゴリズム②を改良することでさらに高速化することが可能です。
アルゴリズム②は次のような変換手続きを n 回繰り返すことで b が求めたいフィボナッチ数になるというアルゴリズムでした。
ここで、この変換を次のような変換 Tpq の p = 0, q = 1 であるときの特殊なケースであるとみなします。
a ← aq + bq + ap
b ← bp + aq
そしてこの変換 Tpq を a と b に対して2回行う変換を Tp’q’ とします。Tp’q’ はTpq 変換を2回行った結果と同じ結果を求める変換なので、この変換では最終的な答えを求めるのに変換を行う回数が、変換 Tpq の半分になります。
a0, b0 を a, b に展開し整理
a” ← a(2pq + p^2) + b(2pq + フィボナッチ数 フィボナッチ数 p^2) +a(p^2 + q^2)
b” ← b(p^2 + q^2) + a(2pq + p^2)
になり、したがって p’ = p^2 + q^2, q’ = 2pq フィボナッチ数 + p^2 になります。
これを利用したフィボナッチ数を求めるアルゴリズムは次のとおりです。
このアルゴリズムではフィボナッチ数を O(log n) で求めることができます。
このアルゴリズムでは n = 1000000 くらいまでならそこそこの時間で求めることができます。(ここまでくると、BigInteger の加減乗除が O(n) や O(2^n) で効いてくるのであんまり速くなりません)
フィボナッチ数
再帰使わない
Golangのsliceを使って実装しています。こうすれば再帰は使わずに実装できますね
ではベンチマーク行ってみましょう
ループの回数を減らしてみる
今までは i 分だけ回してましたが、 i/2 分だけにしてみましょう
だんだん読みづらくなってきました…
ベンチマーク行ってみましょう
17.4ns!!どんどん早くなって行ってますね
ここまでは自分で工夫して行きましたがそろそろ限界なのでいろいろ調べてみましょう
Golangのパフォーマンスチューニング
- メモリのアロケーション回数を減らす
- 要素数が事前にわかっている場合には append を使わない
- channel を使わない
- 関数(メソッド)を呼ばない
などなど…
いろいろ調べましたが、今回のコードで悪いところといえばsliceを使っているところですかね?(本来はトリニボッチとかに対応するためにsliceにしたのですが…)
ではこれをただのIntの変数に変えてみましょう
これが一番早いと思います
見る限り爆速そうですね(適当)
ではベンチマークしてみましょう
え?5.20ns?めちゃくちゃ早くなった!!!!
やっぱり毎回sliceでアクセスするのは遅いですね(というかsliceの値にアクセスするのに%演算子要らなくないか…?)
コメント