アルゴリズム分析の魔法の背後にあるシステムはありますか?


159

アルゴリズムの実行時間を分析する方法については、多くの質問があります(たとえば、および参照)。多くは似ています。たとえば、ネストされたループのコスト分析や、分割統治アルゴリズムを求めるものですが、ほとんどの答えはオーダーメイドのようです。

一方、別の一般的な質問への回答では、いくつかの例を使用してより大きな全体像(特に漸近解析に関する)を説明していますが、手を汚す方法はありません。

アルゴリズムのコストを分析するための構造化された一般的な方法はありますか?コストは、実行時間(時間の複雑さ)、または実行された比較の数、スペースの複雑さなどのコストのその他の尺度である場合があります。

これは、初心者を指すのに使用できる参照質問になるはずです。したがって、通常よりも広い範囲です。少なくとも1つの例で説明されているが、多くの状況をカバーする、一般的で教訓的に提示された答えを与えるように注意してください。ありがとう!


3
StackEditの著者にこのような長い投稿を書くのを便利にしてくれたことに感謝し、私のベータリーダーFrankWJuhoGilles、およびSebastianに以前のドラフトが持っていた多くの欠陥を解決するのを助けてくれました。
ラファエル

1
@Raphaelさん、これは素晴らしいものです。私はそれをPDFとしてまとめて回すことを提案すると思いましたか?この種のものは、本当に便利なリファレンスになる可能性があります。
14

1
@hadsed:ありがとう、私はそれがあなたにとって役に立つことをうれしく思います!今のところ、私はこの投稿へのリンクが回覧されることを好みます。ただし、SEユーザーコンテンツは「cc by-sa 3.0でライセンスが必要であり、属性が必要です」(ページフッターを参照)ので、属性が与えられている限り、誰でもそこからPDFを作成できます。
ラファエル

2
私はこれについて特に有能ではありませんが、どの回答にもマスター定理への言及がないのは普通ですか?
babou

1
@babouここで「通常」の意味がわかりません。私の観点からすると、マスター定理にはビジネスがありません。これはアルゴリズムの分析に関するものであり、マスター定理は(一部の)再発を解決するための非常に特殊なツールです(非常に大雑把に)。数学は他の場所(例:ここ)で説明されているので、ここではアルゴリズムから数学までの部分のみを説明することにしました。私の答えでは、数学の動作を扱う投稿への参照を提供します。
ラファエル

回答:


134

コードを数学に変換する

(多かれ少なかれ)正式な操作上のセマンティクスがあれば、アルゴリズムの(擬似)コードを、文字通り、結果を提供する数式に変換できます。これは、比較、スワップ、ステートメント、メモリアクセス、いくつかの抽象的なマシンニーズのサイクル数などの追加コスト測定に適しています。

例:Bubblesortでの比較

与えられた配列をソートするこのアルゴリズムを考えてみましょうA

 bubblesort(A) do                   1
  n = A.length;                     2
  for ( i = 0 to n-2 ) do           3
    for ( j = 0 to n-i-2 ) do       4
      if ( A[j] > A[j+1] ) then     5
        tmp    = A[j];              6
        A[j]   = A[j+1];            7
        A[j+1] = tmp;               8
      end                           9
    end                             10
  end                               11
end                                 12

通常のソートアルゴリズム分析を実行するとします。つまり、要素比較の数をカウントします(5行目)。この量は配列の内容に依存せずA、長さのみ依存することにすぐに注意してください。したがって、(ネストされた)ループを文字通り(ネストされた)合計に変換できます。ループ変数が合計変数になり、範囲が引き継がれます。我々が得る:nfor

Ccmp(n)=i=0n2j=0ni21==n(n1)2=(n2)

ここで、は5行目の実行ごとのコストです(カウント)。1

例:Bubblesortのスワップ

Iは、によって表すう行で構成サブプログラムへとによってこのサブプログラムを実行するためのコスト(1回)。 C i jPi,jijCi,j

スワップをカウントしたいとしましょう。これは、が実行される頻度です。これは「基本ブロック」、つまり常にアトミックに実行され、一定のコスト(ここでは)を持つサブプログラムです。そのようなブロックを縮小することは、考えたり話したりせずに適用することが多い便利な単純化です。 1P6,81

上記と同様の翻訳で、次の式が得られます。

Cswaps(A)=i=0n2j=0ni2C5,9(A(i,j))

A(i,j)は、の番目の反復の前の配列の状態を示します。(i,j)P5,9

パラメーターとしてではなくを使用していることに注意してください。その理由はすぐにわかります。コストはここで(つまり、均一コストモデル)に依存しないため、パラメーターとしてとを追加しません。一般に、彼らはただそうするかもしれません。AnijC5,9

明らかに、コストはの内容(値および、特に)に依存するため、それを考慮する必要があります。今、私たちは課題に直面しています:をどのように「アンラップ」しますか?さて、のコンテンツへの依存関係を明示的にすることができます。P5,9AA[j]A[j+1]C5,9A

C5,9(A(i,j))=C5(A(i,j))+{1,A(i,j)[j]>A(i,j)[j+1]0,else

どの入力配列でも、これらのコストは明確に定義されていますが、より一般的なステートメントが必要です。より強い仮定をする必要があります。3つの典型的なケースを調べてみましょう。

  1. 最悪の場合

    合計を見て、に注目するだけで、コストの自明な上限を見つけることができます。C5,9(A(i,j)){0,1}

    Cswaps(A)i=0n2j=0ni21=n(n1)2=(n2)

    しかし、これは起こる可能性があります。つまり、この上限のが達成されるのでしょうか。結局のところ、はい:ペアごとに異なる要素の逆ソートされた配列を入力した場合、すべての反復でスワップを実行する必要があります¹。したがって、Bubblesortのスワップの正確な最悪ケース数を導き出しました。A

  2. 最高のケース

    逆に、簡単な下限があります。

    Cswaps(A)i=0n2j=0ni20=0

    これは、すでにソートされている配列でも発生する可能性があります。Bubblesortは単一のスワップを実行しません。

  3. 平均的なケース

    最悪で最良の場合は、かなりのギャップが生じます。しかし、スワップの典型的な数は何ですか?この質問に答えるためには、「典型的な」が意味するものを定義する必要があります。理論的には、ある入力を別の入力よりも優先する理由はないので、通常、すべての可能な入力にわたって均一な分布を仮定します。つまり、すべての入力が等しく発生する可能性があります。ペアごとに異なる要素を持つ配列に制限しているため、ランダム置換モデルを想定しています。

    次に、この²のようにコストを書き換えることができます。

    E[Cswaps]=1n!Ai=0n2j=0ni2C5,9(A(i,j))

    ここで、単純な合計の操作を超えなければなりません。アルゴリズムを見ると、すべてのスワップが 1つの反転のみを削除することに注意してください(私たちは隣人だけをスワップします³)。すなわち、上で行わスワップの数であり、正確に反転数であるの。したがって、内側の2つの合計を置き換えて、AAinv(A)A

    E[Cswaps]=1n!Ainv(A)

    幸運なことに、反転の平均数は

    E[Cswaps]=12(n2)

    これが最終結果です。これは最悪の場合のコストのちょうど半分であることに注意してください。


  1. アルゴリズムはi = n-1、何も実行しない外部ループの「最後の」反復が実行されないように慎重に定式化されていることに注意してください。
  2. 「」は「期待値」の数学表記であり、ここでは単なる平均です。E
  3. 我々は、道に沿って学習しないだけ隣接する要素をスワップアルゴリズムは(さえ平均して)漸近的に速いバブルソートよりなることはできません-反転の数は、すべてのそのようなアルゴリズムの下限です。これは、たとえばInsertion SortおよびSelection Sortに適用されます。

一般的な方法

例では、制御構造を数学に変換する必要があることがわかりました。翻訳ルールの典型的なアンサンブルを紹介します。また、任意のサブプログラムのコストが現在の状態、つまり(大まかに)変数の現在の値に依存することもわかりました。アルゴリズムは(通常)状態を変更するため、一般的な方法は表記がやや面倒です。混乱し始めたら、例に戻るか、自分で構成することをお勧めします。

で現在の状態を示します(変数の割り当てのセットとして想像してください)。状態で始まるプログラムを実行すると、状態(終了が提供されます)。ψPψψ/PP

  • 個別の声明

    ステートメントが1つだけのS;場合、コストを割り当てます。通常、これは定数関数です。CS(ψ)

  • 表現

    Eフォームの式E1 ∘ E2(たとえば、加算または乗算が可能な算術式)がある場合、コストを再帰的に加算します。

    CE(ψ)=c+CE1(ψ)+CE2(ψ)

    ご了承ください

    • 操作コスト一定であるが、の値に依存しないことがありととcE1E2
    • 式の評価は多くの言語で状態を変えるかもしれません。

    そのため、このルールに柔軟に対応する必要があります。

  • シーケンス

    プログラムをプログラムのPシーケンスとして指定Q;Rすると、コストを

    CP(ψ)=CQ(ψ)+CR(ψ/Q)

  • 条件付き

    Pの形式のプログラムを考えるif A then Q else R endと、コストは州によって異なります。

    CP(ψ)=CA(ψ)+{CQ(ψ/A),A evaluates to true under ψCR(ψ/A),else

    一般に、評価Aは状態を非常によく変更する可能性があるため、個々のブランチのコストの更新が行われます。

  • ループ用

    Pの形式のプログラムを指定するfor x = [x1, ..., xk] do Q endと、コストを割り当てます

    CP(ψ)=cinit_for+i=1kcstep_for+CQ(ψi{x:=xi})

    ここで、はvalueの処理前の状態、つまり、...、... に設定された反復後の状態です。ψiQxixx1xi-1

    ループメンテナンス用の追加の定数に注意してください。ループ変数を作成し()、その値を割り当てる必要があります()。これは関連しているcinit_forcstep_for

    • 次の計算にはxiコストがかかり、
    • for空のボディを持つ-loop(たとえば、特定のコストでベストケース設定で単純化した後)は、反復を実行する場合、ゼロコストになりません。
  • While-Loops

    Pの形式のプログラムを指定するwhile A do Q endと、コストを割り当てます

    CP(ψ) =CA(ψ)+{0,A evaluates to false under ψCQ(ψ/A)+CP(ψ/A;Q), else

    アルゴリズムを調べることにより、この繰り返しは、forループの場合と同様の合計としてうまく表現できることがよくあります。

    例:この短いアルゴリズムを考えてみましょう:

    while x > 0 do    1
      i += 1          2
      x = x/2         3
    end               4
    

    ルールを適用することにより、

    C1,4({i:=i0;x:=x0}) =c<+{0,x00c+=+c/+C1,4({i:=i0+1;x:=x0/2}), else

    個々のステートメントに一定のコストが伴います。これらは状態(およびの値)に依存しないと暗黙的に仮定します。これは「現実」に当てはまる場合も、そうでない場合もあります。cix

    この再発を解決する必要があります。ループ本体のコストではなく、反復回数もの値に依存しないことに注意してください。この繰り返しが残っています:C1,4i

    C1,4(x)={c>,x0c>+c+=+c/+C1,4(x/2), else

    これは、と解決基本的な手段

    C1,4(ψ)=log2ψ(x)(c>+c+=+c/)+c>

    完全な状態を象徴的に再導入します。もし、その後。ψ={,x:=5,}ψ(x)=5

  • 手続き呼び出し

    プログラムを考えるとP、フォームのM(x)いくつかのパラメータ(複数可)のための(名前付き)のパラメータと手順があり、コストを割り当てますxMp

    CP(ψ)=ccall+CM(ψglob{p:=x})

    再度、追加の定数注意してください(実際には依存する可能性があります!)。プロシージャコールは、実際のマシンでの実装方法のために高価であり、ランタイムを支配することさえあります(たとえば、フィボナッチ数の繰り返しを単純に評価する)。ccallψ

    ここでは、状態に関するセマンティックの問題について説明します。グローバル状態とそのようなローカルプロシージャコールを区別する必要があります。ここでグローバル状態のみを渡しM、の値を設定することで初期化された新しいローカル状態を取得すると仮定pxます。さらに、x渡す前に(通常)評価されると想定される式である場合があります。

    例:手順を検討する

    fac(n) do                  
      if ( n <= 1 ) do         1
        return 1               2
      else                     3
        return n * fac(n-1)    4
      end                      5
    end                        
    

    ルールに従って、以下を取得します。

    Cfac({n:=n0})=C1,5({n:=n0})=c+{C2({n:=n0}),n01C4({n:=n0}), else=c+{creturn,n01creturn+c+ccall+Cfac({n:=n01}), else

    fac明らかにアクセスしないため、グローバル状態を無視することに注意してください。この特定の再発は簡単に解決できます

    Cfac(ψ)=ψ(n)(c+creturn)+(ψ(n)1)(c+ccall)

典型的な擬似コードで発生する言語機能について説明しました。高レベルの擬似コードを分析するときは、隠れたコストに注意してください。疑わしい場合は展開します。表記は面倒に思えるかもしれませんが、確かに好みの問題です。ただし、リストされている概念は無視できません。ただし、ある程度の経験があれば、「問題のサイズ」や「頂点の数」など、状態のどの部分がどのコストメジャーに関連しているかをすぐに確認できます。残りはドロップすることができます-これは物事を大幅に簡素化します!

あなたは、これはあまりにも複雑であることを今考えられる場合は、助言すること:それはあります!実際のマシンに非常に近いモデルでアルゴリズムの正確なコストを導き出して、実行時の予測(相対的なものであっても)を可能にするのは困難な試みです。そして、それは実際のマシンでのキャッシュやその他の厄介な効果を考慮していません。

したがって、アルゴリズム分析は、数学的に扱いやすいという点まで単純化されることがよくあります。たとえば、正確なコストを必要としない場合は、任意の時点で(上限または下限の場合)過大または過小評価することができます:定数セットの削減、条件の削除、合計の単純化など。

漸近コストに関する注意

通常、文献やWebで見つかるのは「Big-Oh分析」です。適切な用語は漸近解析です。これは、例で行ったように正確なコストを導出するのではなく、一定の係数と制限内のコストのみを与えることを意味します(大まかに言うと、「」)。n

マシン、オペレーティングシステム、およびその他の要因に応じて、抽象的なステートメントには実際にはいくつかの(一般に不明な)コストがあるため、これは(多くの場合)公平です。短いランタイムは、最初にプロセスをセットアップするオペレーティングシステムによって支配される場合があります。とにかく、あなたはいくらかの動揺を得る。

漸近解析がこのアプローチにどのように関係するかを以下に示します。

  1. 支配的な操作(コストを引き起こす)、つまり最も頻繁に発生する操作(一定の要因まで)を特定します。Bubblesortの例で考えられる選択肢の1つは、5行目の比較です。

    または、基本操作のすべての定数を最大値(上から)でバインドします。それらの最小値(下から)と通常の分析を実行します。

  2. この操作の実行回数をコストとして使用して分析を実行します。
  3. 簡素化する場合、推定を許可します。目標が上限()である場合にのみ、上からの推定を許可するように注意してください。下限()が必要な場合は、下から。OΩ

Landauシンボルの意味を理解しください。このような境界は3つのケースすべてに存在することを忘れないでください。を使用しても、最悪の場合の分析を意味するわけではありません。O

参考文献

アルゴリズム分析にはさらに多くの課題とコツがあります。ここにいくつかの推奨読書があります。

これに類似した手法を使用するタグ付けされた質問が数多くあります。


1
多分、漸近解析のためのマスター定理(およびその拡張)への参照と例
ニコスM.

@NikosMここでは範囲外です(上記の質問に関するコメントも参照してください)。マスター定理などを提示する再発の解決に関する参考記事にリンクしていることに注意してください。
ラファエル

@Nikos M:私の0.02ドル:マスター定理はいくつかの繰り返しで機能しますが、他の多くでは機能しません。再発を解決する標準的な方法があります。そして、実行時間を与える再発さえもしないアルゴリズムがあります。高度なカウント手法が必要になる場合があります。数学の経験が豊富な人には、SedgewickとFlajoletの素晴らしい本「Analysis of Algorithms」をお勧めします。時折の例としてデータ構造が現れますが、焦点はメソッドにあります!
ジェイ

@Raphael運用上のセマンティクスに基づいたこの「コードから数学への変換」方法について、ウェブ上で言及がありません。これをより正式に扱っている本、論文、記事への参照を提供できますか?または、これがあなたによって開発された場合、あなたはより深い何かを持っていますか?
Wyvern666

1
@ Wyvern666残念ながら、いいえ。誰かがこのようなものを作ると主張できる限り、私はそれを自分で作りました。いつか自分で引用可能な作品を書くかもしれません。とはいえ、分析的組み合わせ論(Flajolet、Sedgewick、および他の多く)を取り巻く作業全体がこの基盤です。ほとんどの場合、「コード」の正式なセマンティクスを気にしませんが、一般的に「アルゴリズム」の追加コストに対処するための数学を提供します。正直に言って、ここで説明する概念はそれほど深くはありません。あなたが入ることができる数学はそうです。
ラファエル

29

ステートメントの実行カウント

ドナルドE.クヌースが彼のThe Art of Computer Programmingシリーズ支持している別の方法があります。アルゴリズム全体を1つの式変換するのとは対照的に、「物事をまとめる」側のコードのセマンティクスとは独立して機能し、「イーグルズアイ」ビューから開始して、必要な場合にのみ下位レベルに移動できます。すべてのステートメントは、他のステートメントとは無関係に分析でき、より明確な計算につながります。ただし、この手法は、より高度な擬似コードではなく、かなり詳細なコードに適しています。

方法

原理的には非常に簡単です:

  1. すべてのステートメントに名前/番号を割り当てます。
  2. すべてのステートメントコストます。SiCi
  3. すべてのステートメントの実行回数決定します。Siei
  4. 総コストを計算する

    C=ieiCi

推定値および/またはシンボリック量を任意のポイントに挿入できるため、それぞれを弱めます。それに応じて結果を一般化します。

手順3は任意に複雑になる可能性があることに注意してください。通常は、結果を取得するために「」などの(漸近的な)推定値を使用する必要があります。e77O(nlogn)

例:深さ優先検索

次のグラフ走査アルゴリズムを検討してください。

dfs(G, s) do
  // assert G.nodes contains s
  visited = new Array[G.nodes.size]     1
  dfs_h(G, s, visited)                  2
end 

dfs_h(G, s, visited) do
  foo(s)                                3
  visited[s] = true                     4

  v = G.neighbours(s)                   5
  while ( v != nil ) do                 6
    if ( !visited[v] ) then             7
      dfs_h(G, v, visited)              8
    end
    v = v.next                          9
  end
end

(無向)グラフは、ノード隣接リストによって与えられると仮定します。してみましょう辺の数です。{0,,n1}m

アルゴリズムを見るだけで、一部のステートメントが他のステートメントと同じ頻度で実行されることがわかります。実行カウントいくつかのプレースホルダー、、およびを導入します。ABCei

i123456789eiAABBBB+CCB1C

特に、は、8行目のすべての再帰呼び出しが3行目の呼び出しを引き起こします(1つはからの元の呼び出しが原因です)。さらに、。条件は反復ごとに1回チェックする必要がありますが、それを終了するにはもう一度チェックする必要があります。e8=e31foodfse6=e5+e7while

あることは明らかです。さて、正当性の証明中に、ノードごとに1回だけ実行されることを示します。つまり、です。しかし、その後、すべての隣接リストを正確に1回反復し、すべてのエッジが合計2つのエントリ(インシデントノードごとに1つ)を意味します。合計反復を取得します。これを使用して、次の表を導き出します。A=1fooB=nC=2m

i123456789ei11nnn2m+n2mn12m

これにより、正確に合計コストが発生します

C(n,m)=(C1+C2C8)+ n(C3+C4+C5+C6+C8)+ 2m(C6+C7+C9).

適切な値をインスタンス化することにより、より具体的なコストを導き出すことができます。たとえば、メモリアクセスを(ワードごとに)カウントする場合は、次を使用します。Ci

i123456789Cin00110101

そして得る

Cmem(n,m)=3n+4m

参考文献

私の他の答えの一番下を参照してください。


8

定理証明のようなアルゴリズム分析は、大部分が芸術です(たとえば、分析方法がわからない単純なプログラム(Collat​​z問題など)があります)。Raphaelが包括的に回答したように、アルゴリズムの複雑さの問題を数学的な問題に変換できますが、既知の関数に関してアルゴリズムのコストの限界を表現するために、次のことを行います。

  1. 既存の分析でわかっている手法を使用します。たとえば、理解している繰り返しに基づいて境界を見つけたり、計算できる合計/積分を使用したりします。
  2. アルゴリズムを分析方法がわかっているものに変更します。
  3. まったく新しいアプローチを考え出します。

1
私はこれが他の答えに加えて有用で新しいものを追加する方法を見ていないと思います。テクニックはすでに他の回答で説明されています。これは、質問に対する答えというよりも、コメントのように見えます。
DW

1
他の答えは、それが芸術ではないことを証明していると思います。あなたはそれをすることができないかもしれません(すなわち数学)、そしてあなたがそうであってもいくらかの創造性(既知の数学をどのように適用するかに関して)が必要かもしれませんが、それはどんなタスクに当てはまります。ここでは新しい数学を作成することを望んでいないと思います。(実際、この質問とその回答は、プロセス全体を分かりやすく説明することを目的としていました。)
ラファエル

4
@Raphael Ariは、「プログラムによって実行される命令の数」(答えは答えです)ではなく、認識可能な関数をバインドとして作成することについて話しています。一般的なケース芸術です。すべてのアルゴリズムに自明でない限界を思い付くことができるアルゴリズムはありません。ただし、一般的なケースは、既知の手法(マスター定理など)のセットです。
ジル

@Gillesアルゴリズムが存在しないものがすべてアートである場合、職人(特にプログラマー)の賃金はさらに低下します。
ラファエル

1
@AriTrachlenbergは重要なポイントを示しますが、アルゴリズムの時間の複雑さを評価する方法は無数にあります。Big O表記の定義自体は、著者に応じて、その理論的な性質を暗示するか、直接述べています。「最悪の場合のシナリオ」は、議論や議論の場にいるN人の中で、推測や新しい事実の余地を明らかに残しています。漸近的推定の本質が何かであるということは言うまでもありません...よくわかりません。
ブライアンオグデン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.