フィボナッチ数列の計算の複雑さ


330

Big-O表記は理解できますが、多くの関数で計算する方法がわかりません。特に、フィボナッチ数列の単純なバージョンの計算の複雑さを理解しようと試みてきました。

int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

フィボナッチ数列の計算の複雑さはどのくらいで、どのように計算されますか?



3
こちらのマトリックスフォームセクションを参照してください:en.wikipedia.org/wiki/Fibonacci_number。この行列^ n(巧妙な方法で)を実行することにより、O(lg n)でFib(n)を計算できます。トリックは、べき関数を実行することです。この正確な問題とO(lg n)での解決方法について、iTunesUに関する非常に優れた講義があります。このコースはMIT講義3のアルゴリズムの紹介です(そのアブソリュートリーは無料です。興味があればチェックしてください)
Aly

1
上記のコメントはどちらも、(投稿されたコード内の)単純なバージョンの計算の複雑さに関する問題に対処していません。マトリックス形式や非再帰的な計算などのよりスマートなバージョンに関するものではありません。
ジョシュミルソープ2018年

再帰的な実装の下限の複雑度(2 ^ n / 2)と上限の複雑度(2 ^ n)の両方について説明している非常に素晴らしいビデオです
RBT 2019年

1
副次的な質問:フィボナッチシリーズの単純な実装は反復的再帰的か?
RBT 2019年

回答:


374

あなたは計算に時間関数をモデルFib(n)計算するための時間の合計としてFib(n-1)計算するには、プラス時間Fib(n-2)(一緒にそれらを追加するには、プラス時間O(1))。これは、同じものを繰り返し評価するFib(n)のに同じ時間がかかる、つまりメモを使用しないことを前提としています。

T(n<=1) = O(1)

T(n) = T(n-1) + T(n-2) + O(1)

この再帰関係を(たとえば、生成関数を使用して)解決すると、答えが得られます。

または、深さを持ちn、この関数が漸近的になることを直感的に理解する再帰ツリーを描画することもできます。次に、帰納法によって推測を証明できます。O(2n)

ベース:n = 1明らか

想定、したがって、T(n-1) = O(2n-1)

T(n) = T(n-1) + T(n-2) + O(1) に等しい

T(n) = O(2n-1) + O(2n-2) + O(1) = O(2n)

ただし、コメントに記載されているように、これは厳しい制限ではありません。この機能についての興味深い事実は、T(n)が漸近的であることである同一の値としてFib(n)、両方のように定義されているので

f(n) = f(n-1) + f(n-2)

再帰ツリーの葉は常に1を返します。の値はFib(n)、再帰ツリーの葉によって返されるすべての値の合計で、葉の数と同じです。各葉は計算にO(1)を使用するため、T(n)はに等しくなりFib(n) x O(1)ます。したがって、この関数のタイトな境界はフィボナッチ数列自体(〜)です。上記で述べたように生成関数を使用することで、このタイトな境界を見つけることができます。θ(1.6n)


29
また、誘導による証明。いいね。+1
アンドリューローリングス

限界はきつくありませんが
キャプテンSegfault

@キャプテンセグフォルト:うん。答えを明確にしました。上で書いたように、GFメソッドを使用すると、きつい境界が得られます。
Mehrdad Afshari、

StackOverflowExceptionをジョークとして取ります。指数時間は、nの値がかなり小さいと非常に簡単に認識できます。
デビッドロドリゲス-ドリベス

1
「あるいは、深さnを持つ再帰ツリーを描画して、この関数が漸近的にO(2n)であることを直感的に理解することができます。」-これは完全に誤りです。時間の複雑さはO(golden_ratio ^ n)です。O(2 ^ n)に近づくことはありません。無限に向かって手を伸ばせば、O(golden_ratio ^ n)に近くなります。すなわち、漸近線が何であるかである2つのライン間の距離は0に接近しなければならない
ボブ

133

F(n)完了するために実行する必要があるステートメントの数を自問してください。

以下の場合F(1)、答えは1(条件付きの最初の部分)。

以下の場合F(n)、答えはありますF(n-1) + F(n-2)

それでは、どの関数がこれらのルールを満たしていますか?a n(a> 1)を試してください:

a n == a (n-1) + a (n-2)

(n-2)で除算する:

a 2 == a + 1

解決するa(1+sqrt(5))/2 = 1.6180339887黄金比とも呼ばれます。

したがって、指数関数的な時間がかかります。


8
誘導による証明。いいね。+1
アンドリューローリングス

2
間違った答えに賛成30票?:-) 1 = F(1)=(1 + sqrt(5))/ 2ということになりますか?そして、他のソリューション(1-sqrt(5))/ 2はどうですか?
Carsten S

1
いいえ、1は1 + 1と同じではありません。これらのルールを満足する関数は質問で言及されています。
molbdnilo 2014

6
答えは間違っていません。それは無症状で正しいです。他の解決策は否定的であるため、物理的に意味がありません。
Da Teng

10
誰かがa ^ n == a ^(n-1)+ a ^(n-2)がこれらのルールをどのように満たすか説明できますか?正確にどのように満足していますか、具体的にご記入ください。
率直

33

私はpgaurとrickerbhに同意します。再帰的なフィボナッチの複雑さはO(2 ^ n)です。

私はかなり単純化して同じ結論に達しましたが、私はまだ有効な推論を信じています。

まず、N番目のフィボナッチ数を計算するときに、再帰的なフィボナッチ関数(F()が今後何回呼び出されるか)を把握することがすべてです。0からnのシーケンスで数値ごとに1回呼び出されると、O(n)が得られ、数値ごとにn回呼び出されると、O(n * n)、またはO(n ^ 2)が得られます。等々。

したがって、nに対してF()が呼び出されると、0からn-1までの任意の数に対してF()が呼び出される回数は、0に近づくにつれて大きくなります。

第一印象として、視覚的に言えば、与えられた数値に対してF()が呼び出されるたびにユニットを描画すると、一種のピラミッド形状がウェットになる(つまり、ユニットを水平方向に中央揃えすると) )。このようなもの:

n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

さて、問題は、nが大きくなるにつれて、このピラミッドのベースがどれだけ速く拡大するかです。

実際のケースを考えてみましょう、例えばF(6)

F(6)                 *  <-- only once
F(5)                 *  <-- only once too
F(4)                 ** 
F(3)                ****
F(2)              ********
F(1)          ****************           <-- 16
F(0)  ********************************    <-- 32

F(0)が32回呼び出されることがわかります。これは2 ^ 5です。このサンプルケースでは2 ^(n-1)です。

ここで、F(x)が呼び出される回数を知りたいと思います。F(0)が呼び出される回数はその一部にすぎません。

すべての*をF(6)行からF(2)行にF(1)行に精神的に移動すると、F(1)行とF(0)行の長さが等しくなっていることがわかります。つまり、n = 6が2x32 = 64 = 2 ^ 6の場合、F()の合計時間が呼び出されます。

さて、複雑さに関しては:

O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)

3
F(3)は3回だけ呼び出され、4回は呼び出されません。2番目のピラミッドは間違っています。
Avik 2016

2
F(3)= 3、F(2)= 5、F(1)= 8、F(0)= 5.私はそれを修正しますが、編集してこの答えを救えるとは思いません。
Bernhard Barker 2017年

31

MITでは、この特定の問題について非常に素晴らしい議論があります。5ページでは、加算が1計算単位をとると仮定すると、Fib(N)の計算に必要な時間はFib(N)の結果と非常に密接に関連していると彼らは指摘しています。

その結果、フィボナッチシリーズの非常に近い近似に直接スキップできます。

Fib(N) = (1/sqrt(5)) * 1.618^(N+1) (approximately)

したがって、単純なアルゴリズムの最悪の場合のパフォーマンスは

O((1/sqrt(5)) * 1.618^(N+1)) = O(1.618^(N+1))

PS:詳細については、WikipediaでN番目のフィボナッチ数閉形式の表現についての議論があります。


コースのリンクをありがとう。非常に素晴らしい観察
SwimBikeRun 2018

16

あなたはそれを拡張して視覚化することができます

     T(n) = T(n-1) + T(n-2) <
     T(n-1) + T(n-1) 

     = 2*T(n-1)   
     = 2*2*T(n-2)
     = 2*2*2*T(n-3)
     ....
     = 2^i*T(n-i)
     ...
     ==> O(2^n)

1
最初の行を理解しました。しかし、なぜ<最後に「より小」の文字があるのですか?どうやって手に入れたのT(n-1) + T(n-1)
Quazi Irfan 2017

@QuaziIrfan:Dそれは矢です。-> [(以上)。最後の行に関して混乱して申し訳ありません]。最初の行については、まあ... T(n-1) > T(n-2)だから私は変更T(n-2)して置くことができますT(n-1)。私はまだ有効な上限のみを取得しますT(n-1) + T(n-2)
トニー・タナス

10

2^(n/2)(他のコメントに記載されているように)下限は2 ^ n、上限は2 ^ nです。そして、その再帰的な実装の興味深い事実は、Fib(n)自体の厳密な漸近的境界があることです。これらの事実は要約することができます:

T(n) = Ω(2^(n/2))  (lower bound)
T(n) = O(2^n)   (upper bound)
T(n) = Θ(Fib(n)) (tight bound)

タイトバウンドは、必要に応じて閉じたフォームを使用してさらに減らすことができます。


10

証明の答えは良いですが、私は本当に自分自身を納得させるために常に手で数回の反復を行わなければなりません。そこで、ホワイトボードに小さな呼び出しツリーを描き、ノードの数を数え始めました。カウントを合計ノード、リーフノード、内部ノードに分割します。ここに私が得たものがあります:

IN | OUT | TOT | LEAF | INT
 1 |   1 |   1 |   1  |   0
 2 |   1 |   1 |   1  |   0
 3 |   2 |   3 |   2  |   1
 4 |   3 |   5 |   3  |   2
 5 |   5 |   9 |   5  |   4
 6 |   8 |  15 |   8  |   7
 7 |  13 |  25 |  13  |  12
 8 |  21 |  41 |  21  |  20
 9 |  34 |  67 |  34  |  33
10 |  55 | 109 |  55  |  54

すぐに飛び出すのは、葉ノードの数がであることですfib(n)。さらにいくつかの反復に気づいたのは、内部ノードの数がであることですfib(n) - 1。したがって、ノードの総数は2 * fib(n) - 1です。

計算の複雑さを分類するときに係数を削除するため、最終的な答えはθ(fib(n))です。


(いいえ、ホワイトボードに完全な10深さの呼び出しツリーを描画しませんでした。5深さだけです。);)
benkc 14

いいですね、再帰的なFibが何回余分に追加したのかと思っていました。1単一のアキュムレータFib(n)時間に追加するだけでなく、それがまだ正確であることは興味深いですθ(Fib(n))
Peter Cordes、2018年

注いくつかの(ほとんど)再帰的な実装では、追加の時間を過ごすこと0が、:再帰ベースケースがある01彼らが行うので、Fib(n-1) + Fib(n-2)。だから、おそらく3 * Fib(n) - 2から、このリンク-唯一の答えは、全ノード数のため、より正確ではありません2 * Fib(n) - 1
Peter Cordes、2018年

葉ノードで同じ結果を得ることができません。0から開始:F(0)-> 1リーフ(それ自体); F(1)-> 1リーフ(それ自体); F(2)-> 2リーフ(F(1)およびF(0)); F(3)-> 3リーフ; F(5)-> 8葉; など
alexlomba87

9

再帰アルゴリズムの時間の複雑さは、再帰ツリーを描画することでより適切に推定できます。この場合、再帰ツリーを描画するための再帰関係はT(n)= T(n-1)+ T(n-2)+ O(1)になります。各ステップは、一定の時間を意味するO(1)を使用します。これはブロックの場合、nの値をチェックするための比較は1つだけなので再帰ツリーは次のようになります。

          n
   (n-1)      (n-2)
(n-2)(n-3) (n-3)(n-4) ...so on

ここで、上のツリーの各レベルがiで表されるとしましょう。

i
0                        n
1            (n-1)                 (n-2)
2        (n-2)    (n-3)      (n-3)     (n-4)
3   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)

特定のiの値でツリーが終了するとしましょう。その場合、ni = 1、つまりi = n-1の場合になり、ツリーの高さがn-1になります。ここで、ツリー内のn個のレイヤーのそれぞれについてどれだけの作業が行われたかを確認します。再帰関係で述べたように、各ステップにはO(1)時間かかることに注意してください。

2^0=1                        n
2^1=2            (n-1)                 (n-2)
2^2=4        (n-2)    (n-3)      (n-3)     (n-4)
2^3=8   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)    ..so on
2^i for ith level

i = n-1は各レベルで行われる木の作業の高さであるため

i work
1 2^1
2 2^2
3 2^3..so on

したがって、行われた総作業量は、各レベルで行われた作業の合計になります。したがって、i = n-1なので、2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 ... + 2 ^(n-1)になります。幾何級数では、この合計は2 ^ nなので、ここでの合計時間の複雑さはO(2 ^ n)です。


2

まあ、私によればO(2^n)、この関数のように再帰のみがかなりの時間を費やしています(分割統治)。私たちはそれを見葉が、我々はレベルに達したときにアプローチされるまで、上記の関数がツリーに継続されますF(n-(n-1))すなわちF(1)。したがって、ここで、ツリーの各深さで発生する時間の複雑さを書き留めると、総和系列は次のようになります。

1+2+4+.......(n-1)
= 1((2^n)-1)/(2-1)
=2^n -1

それがの順序です2^n [ O(2^n) ]


1

フィボナッチの単純な再帰バージョンは、計算が繰り返されるため、設計上指数関数的です。

ルートで計算しています:

F(n)はF(n-1)とF(n-2)に依存します

F(n-1)は再びF(n-2)とF(n-3)に依存します

F(n-2)は再びF(n-3)とF(n-4)に依存します

次に、各レベルで、計算で大量のデータを浪費している再帰呼び出しがあり、時間関数は次のようになります。

T(n)= T(n-1)+ T(n-2)+ C、C定数あり

T(n-1)= T(n-2)+ T(n-3)> T(n-2)次に

T(n)> 2 * T(n-2)

...

T(n)> 2 ^(n / 2)* T(1)= O(2 ^(n / 2))

これは下限であり、分析の目的には十分ですが、リアルタイム関数は同じフィボナッチ公式による定数の係数であり、閉じた形は黄金比の指数関数であることがわかっています。

さらに、次のような動的プログラミングを使用して、フィボナッチの最適化バージョンを見つけることができます。

static int fib(int n)
{
    /* memory */
    int f[] = new int[n+1];
    int i;

    /* Init */
    f[0] = 0;
    f[1] = 1;

    /* Fill */
    for (i = 2; i <= n; i++)
    {
        f[i] = f[i-1] + f[i-2];
    }

    return f[n];
}

これは最適化され、nステップしか実行しませんが、指数関数的でもあります。

コスト関数は、問題を解決するための入力サイズからステップ数まで定義されます。フィボナッチの動的バージョン(テーブルを計算するnステップ)または数値が素数かどうかを知る最も簡単なアルゴリズム(sqrt(n)で数値の有効な約数を分析)が表示された場合。これらのアルゴリズムはO(n)またはO(sqrt(n))であると考えるかもしれませんが、これは次の理由で単純に当てはまりません:アルゴリズムへの入力は数値です:n、バイナリ表記を使用して、整数nlog2(n)であり、変数の変更を行う

m = log2(n) // your real input size

入力サイズの関数としてステップ数を見つけましょう

m = log2(n)
2^m = 2^log2(n) = n

次に、入力サイズの関数としてのアルゴリズムのコストは次のとおりです。

T(m) = n steps = 2^m steps

これがコストが指数関数的である理由です。


1

関数呼び出しを図式化することで計算するのは簡単です。nの各値に対して関数呼び出しを追加し、数がどのように増加するかを確認します。

ビッグOはO(Z ^ n)で、Zは黄金比または約1.62です。

レオナルド数とフィボナッチ数はどちらも、nを増やすとこの比率に近づきます。

他のBig Oの質問とは異なり、入力にばらつきはなく、アルゴリズムとアルゴリズムの実装の両方が明確に定義されています。

複雑な数学の束は必要ありません。以下の関数呼び出しを簡単に説明し、関数を数値に合わせます。

または、黄金比に精通している場合は、そのように認識します。

この答えは、f(n)= 2 ^ nに近づくと主張する受け入れられた答えよりも正確です。それは決してありません。f(n)= golden_ratio ^ nに近づきます。

2 (2 -> 1, 0)

4 (3 -> 2, 1) (2 -> 1, 0)

8 (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)


14 (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
            (2 -> 1, 0)

            (3 -> 2, 1) (2 -> 1, 0)

22 (6 -> 5, 4)
            (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

                        (3 -> 2, 1) (2 -> 1, 0)

            (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                        (2 -> 1, 0)

1
黄金比についてのその主張の情報源を提供できますか?
ニコHaase

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.