Big-O表記を理解していますが、多くの関数について計算する方法がわかりません。特に、私はフィボナッチ数列の単純版の計算の複雑さを理解しようとしてきました:
int Fibonacci(int n)
{
if (n <= 1)
return n;
else
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
フィボナッチ数列の計算量はどのくらいであり、それはどのように計算されますか?
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(2
であることを直感的に理解します。n
)
。あなたは帰納法によってあなたの推測を証明することができます。
ベース:n = 1
は明らかです
T(n-1) = O(2
と仮定しますn-1
)
、 したがって
T(n) = T(n-1) + T(n-2) + O(1)
これは等しい
T(n) = O(2
n-1
) + O(2
n-2
) + O(1) = O(2
n
)
しかし、コメントで述べたように、これは厳密な限界ではありません。この関数についての興味深い事実は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.6
n
)
)上で述べたように、生成関数を使用することで、この限界を見つけることができます。
F(n)
を完了するためにどれだけのステートメントを実行する必要があるかを自分自身に尋ねてください。
F(1)
の場合、答えは1
(条件式の最初の部分)です。
F(n)
の場合、答えはF(n-1) + F(n-2)
です。
では、どの関数がこれらの規則を満たすのでしょうか。試してみるn (a> 1):
あるn == a(n-1) + a(n-2)
で割る(n-2):
ある2 == a + 1
a
を解くと、(1+sqrt(5))/2 = 1.6180339887
が得られます。そうでなければ 黄金比 として知られています。
だから指数関数的な時間がかかります。
これについては非常に良い議論があります 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))
シモンズ:ウィキペディアで N番目のフィボナッチ数の閉形式表現 についての議論があります。
私はpgaurとrickerbhに同意します、再帰的フィボナッチの複雑さはO(2 ^ n)です。
私はどちらかというと単純化して同じ結論に達しましたが、それでも妥当な推論を信じています。
まず、N番目のフィボナッチ数を計算するときに再帰的フィボナッチ関数(F()以降)が呼び出される回数を計算することがすべてです。それが0からnまでのシーケンスで番号ごとに一度呼ばれるならば、我々はO(n)を持ち、それが各番号に対してn回呼び出されるならばO(n * n)、またはO(n ^ 2)を得る、等々。
したがって、F()がnに対して呼び出されると、0に近づくにつれてF()が0からn-1までの間呼び出される回数が増えます。
第一印象として、視覚的に言えば、単位時間あたりの単位を描くF()が呼ばれるなら、wetは一種のピラミッド形状をとる(つまり、もし我々がユニットを水平に中央に置くならば)。このようなもの:
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)
展開して視覚化することができます
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)
それは下端で2^(n/2)
で、上端で2 ^ nで区切られています(他のコメントで述べたように)。そして、その再帰的な実装の興味深い事実は、それがFib(n)自体の厳しい漸近的な限界を持っているということです。これらの事実は要約することができます。
T(n) = Ω(2^(n/2)) (lower bound)
T(n) = O(2^n) (upper bound)
T(n) = Θ(Fib(n)) (tight bound)
あなたが好きならタイトバウンドは クローズドフォーム を使ってさらに減らすことができます。
証明の答えは良いですが、私は本当に自分自身を納得させるために常に手作業で数回の反復をしなければなりません。それで私は私のホワイトボード上に小さなコーリングツリーを引き出し、そしてノードを数え始めました。カウントを合計ノード、リーフノード、および内部ノードに分割しました。これが私が手に入れたものです:
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))
です。
再帰的アルゴリズムの時間複雑度は、再帰木を描くことによってよりよく推定することができます。この場合、再帰木を描くための再帰関係はT(n)= T(n-1)+ T(n-2)+ O(1)各ステップは、ifblock内のnの値をチェックするための比較を1回だけ行うので、O(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の特定の値でツリーが終了するとしましょう。その場合はn-i = 1、つまりi = n-1のときになり、ツリーの高さはn-1になります。それではtreeの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)です。
さて、私によれば、それは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) ]
の順番です。
フィボナッチの素朴な再帰バージョンは、計算が繰り返されるため、設計上指数関数的になります。
根本的にあなたが計算している:
F(n) depends on F(n-1) and F(n-2)
F(n-1) depends on F(n-2) again and F(n-3)
F(n-2) depends on F(n-3) again and F(n-4)
それで、あなたは計算でたくさんのデータを浪費している各レベル2の再帰呼び出しをしている、time関数はこのようになるでしょう:
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))
これは分析の目的には十分であるべき下限ですが、実時間関数は同じフィボナッチ公式による定数の係数であり、 閉形式 はの指数関数であることが知られています。黄金比。
さらに、次のような動的計画法を使って最適化されたバージョンのFibonacciを見つけることができます。
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ステップだけを実行しますが、指数関数的です。
費用関数は、入力サイズから問題を解決するためのステップ数まで定義されます。動的版のFibonacci(nで表を計算する手順)または数値が素数であるかどうかを知るための最も簡単なアルゴリズム(sqrt(n)数の有効な約数を分析します。これらのアルゴリズムは、O(n)またはO(sqrt(n))しかし、これは単に以下の理由で真実ではありません。あなたのアルゴリズムへの入力は数です:n整数の入力サイズnはlog2(n)です。の変動
m = log2(n) // your real input size
入力サイズの関数としてステップ数を調べましょう
m = log2(n)
2^m = 2^log2(n) = n
その場合、入力サイズの関数としてのアルゴリズムのコストは次のようになります。
T(m) = n steps = 2^m steps
これが、コストが指数関数的な理由です。
http://www.ics.uci.edu/~eppstein/161/960109.html
時間(n)= 3F(n) - 2