bottom-upアプローチ(動的プログラミング)は、最初に「より小さい」副問題を調べ、次に小さな問題の解決策を使用して大きな副問題を解決することにあります。
トップダウンは、「自然な方法」で問題を解決し、以前にサブ問題の解決策を計算したかどうかを確認することで構成されます。
私は少し混乱しています。これら2つの違いは何ですか?
rev4:ユーザーSammaronによる非常に雄弁なコメントは、おそらく、この回答が以前はトップダウンとボトムアップを混同していたことを指摘しています。もともとこの回答(rev3)および他の回答では「ボトムアップはメモ化」(「サブ問題を想定」)と言われましたが、逆の場合もあります(つまり、「トップダウン」は「サブ問題を想定」および「 「ボトムアップ」は「サブ問題を構成する」かもしれません)。以前、動的プログラミングのサブタイプではなく、異なる種類の動的プログラミングであるメモ化について読みました。私はそれを購読していないにもかかわらず、その視点を引用していました。適切な参考文献が文献で見つかるまで、私はこの答えを用語にとらわれないように書き直しました。また、この回答をコミュニティWikiに変換しました。学術資料を優先してください。参照リスト:{Web: 1 、 2 } {文学: 5 }
動的プログラミングとは、重複する作業を再計算しないように計算を順序付けることです。主な問題(サブ問題のツリーのルート)とサブ問題(サブツリー)があります。 副問題は通常繰り返され、重なります。
たとえば、お気に入りのフィボナッチの例を考えてみましょう。単純な再帰呼び出しを行った場合、これはサブ問題の完全なツリーです。
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(他のいくつかのまれな問題では、このツリーは一部のブランチで無限であり、非終了を表している可能性があります。したがって、ツリーの下部は無限に大きくなる場合があります。したがって、どの副問題を明らかにするかを決定するための戦略/アルゴリズムが必要になる場合があります。
動的プログラミングには、相互に排他的ではない少なくとも2つの主要な手法があります。
メモ化-これは自由放任のアプローチです。すべての副問題をすでに計算しており、最適な評価順序が何であるかわからないと仮定します。通常、ルートから再帰呼び出し(または同等の反復呼び出し)を実行し、最適な評価順序に近づくことを希望するか、最適な評価順序に到達するのに役立つ証拠を取得します。結果cacheを使用して再帰呼び出しが副問題を再計算しないようにし、重複したサブツリーが再計算されないようにします。
fib(100)
を計算する場合、これを呼び出すだけで、fib(100)=fib(99)+fib(98)
を呼び出します。 fib(99)=fib(98)+fib(97)
、...などを呼び出し、fib(2)=fib(1)+fib(0)=1+0=1
を呼び出します。その後、最終的にfib(3)=fib(2)+fib(1)
を解決しますが、fib(2)
を再計算する必要はありません。キャッシュしたためです。集計-ダイナミックプログラミングを「テーブル充填」アルゴリズムと考えることもできます(通常、多次元ですが、この「テーブル」は非常にまれなケースで非ユークリッドジオメトリを持つ場合があります*)。これはメモ化に似ていますが、よりアクティブであり、追加のステップが1つ含まれます。計算を行う正確な順序を事前に選択する必要があります。これは、順序が静的でなければならないことを意味するものではなく、メモ化よりもはるかに柔軟性があることを意味する必要があります。
fib(2)
、fib(3)
、fib(4)
...すべての値をキャッシュして、次の値をより簡単に計算できるようにします。また、テーブルがいっぱいになることも考えられます(別の形式のキャッシュ)。(最も一般的な「動的プログラミング」パラダイムでは、プログラマーはツリー全体を考慮していると思います。thenは、戦略を実装するアルゴリズムを記述します任意のプロパティ(通常は時間複雑さと空間複雑さの組み合わせ)を最適化できる副問題を評価するために、戦略は特定の副問題からどこかで開始する必要があり、それらの評価の結果に基づいて適応する可能性があります。 「動的プログラミング」の一般的な意味では、これらの副問題をキャッシュして、より一般的には、さまざまなデータ構造のグラフの場合のように微妙な違いで副問題を再訪しないようにしてください。配列またはテーブル。サブ問題の解決策は、不要になった場合は破棄される可能性があります。)
[以前は、この回答はトップダウンとボトムアップの用語について述べていました。メモ化と集計と呼ばれる2つの主なアプローチがあり、それらの用語と全単射である場合があります(完全ではありません)。ほとんどの人が使用する一般的な用語はまだ「動的プログラミング」であり、「動的プログラミング」の特定のサブタイプを指すために「メモ化」と言う人もいます。この答えは、コミュニティが学術論文で適切な参照を見つけることができるまで、トップダウンとボトムアップのどちらであるかを言うことを拒否します。最終的には、用語ではなく区別を理解することが重要です。]
メモ化は非常に簡単にコーディングできます(一般に、自動的に行う「メモライザー」注釈またはラッパー関数を作成できます*)。最初のアプローチにする必要があります。集計の欠点は、順序付けを考え出す必要があることです。
*(これは実際に自分で関数を記述している場合、および/または不純な/非機能的なプログラミング言語でコーディングしている場合にのみ簡単です...たとえば、誰かがすでにプリコンパイルされたfib
関数を書いている場合、それは必然的に再帰的になりますそれ自体を呼び出し、それらの再帰呼び出しが新しいメモ化された関数を呼び出すことなく、関数を魔法のようにメモすることはできません(元のメモされていない関数)
トップダウンとボトムアップの両方は、再帰的または反復的なテーブル入力で実装できることに注意してください。
メモ化を使用すると、ツリーが非常に深い場合(例:fib(10^6)
)、遅延した各計算をスタックに配置する必要があるため、スタックスペースが不足し、10 ^ 6になります。
発生する(または試行する)副問題を訪問する順序が最適でない場合、特に副問題を計算する方法が複数ある場合、どちらのアプローチも時間的に最適ではない場合があります(通常、キャッシングはこれを解決しますが、キャッシングは理論的には可能です)一部のエキゾチックなケースではありません)。メモ化は通常、時間の複雑さをスペースの複雑さに追加します(たとえば、集計では、Fibで集計を使用することでO(1)スペースを使用できますが、 FibはO(N)スタックスペースを使用します)。
非常に複雑な問題も行っている場合は、集計を行う以外に選択肢がない場合があります(または、少なくとも、メモ化を希望する場所で操作する際により積極的な役割を果たします)。また、最適化が絶対的に重要であり、最適化する必要がある状況にある場合、メモ化では通常の方法ではできない最適化を行うことができます。私の謙虚な意見では、通常のソフトウェアエンジニアリングでは、これら2つのケースはどちらも出てこないので、何か(スタックスペースなど)が集計を必要としない限り、メモ化(「答えをキャッシュする機能」)を使用します...技術的には、スタックブローアウトを回避するために、1)スタック言語を許可する言語でスタックサイズの制限を増やすか、2)スタックを仮想化するために一定の余分な作業を費やす(ick)、または3)継続渡しスタイルのプログラム事実上、スタックも仮想化されます(これの複雑さはわかりませんが、基本的にはサイズNのスタックから遅延呼び出しチェーンを効果的に取り出し、N個の連続してネストされたサンク関数に事実上貼り付けます...テールコールの最適化では、スタックのパンクを回避するために物事をトランポリンする必要があります)。
ここでは、DPの一般的な問題だけでなく、興味深いことにメモ化と集計を区別する、特に興味深い例をリストします。たとえば、1つの定式化が他の定式化よりもはるかに簡単な場合や、基本的に集計を必要とする最適化がある場合があります。
トップダウンとボトムアップDPは、同じ問題を解決する2つの異なる方法です。フィボナッチ数を計算するためのメモ型(トップダウン)対動的(ボトムアップ)プログラミングソリューションを検討してください。
fib_cache = {}
def memo_fib(n):
global fib_cache
if n == 0 or n == 1:
return 1
if n in fib_cache:
return fib_cache[n]
ret = memo_fib(n - 1) + memo_fib(n - 2)
fib_cache[n] = ret
return ret
def dp_fib(n):
partial_answers = [1, 1]
while len(partial_answers) <= n:
partial_answers.append(partial_answers[-1] + partial_answers[-2])
return partial_answers[n]
print memo_fib(5), dp_fib(5)
私は個人的にメモ化がはるかに自然だと感じています。再帰的な関数を取得し、機械的なプロセスでメモすることができます(最初にキャッシュで回答を検索し、可能であればそれを返します。そうでなければ、再帰的に計算してから戻る前に、将来の使用のために計算をキャッシュに保存します)動的プログラミングでは、解が計算される順序をエンコードする必要があります。そのため、依存する小さな問題の前に「大きな問題」が計算されることはありません。
動的プログラミングの重要な機能は、重複する副問題の存在です。つまり、解決しようとしている問題はサブ問題に分割でき、それらのサブ問題の多くはサブサブ問題を共有しています。 「分割して征服する」ようなものですが、同じことを何度も何度も繰り返すことになります。これらの問題を教えたり説明したりするときに2003年以降使用した例: フィボナッチ数 を再帰的に計算できます。
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
お気に入りの言語を使用して、fib(50)
で実行してみてください。非常に長い時間がかかります。 fib(50)
自体とほぼ同じ時間!ただし、多くの不必要な作業が行われています。 fib(50)
はfib(49)
とfib(48)
を呼び出しますが、値が同じであっても、両方ともfib(47)
を呼び出すことになります。実際、fib(47)
は3回計算されます。fib(49)
からの直接呼び出し、fib(48)
からの直接呼び出し、および別のfib(48)
からの直接呼び出しによって、 fib(49)
...の計算によって生成されたものです。つまり、重複する副問題があります。
素晴らしいニュース:同じ値を何度も計算する必要はありません。一度計算したら、結果をキャッシュし、次回はキャッシュされた値を使用します!これが動的プログラミングの本質です。あなたはそれを「トップダウン」、「メモ化」、またはあなたが望む他のものと呼ぶことができます。このアプローチは非常に直感的で、実装が非常に簡単です。最初に再帰的なソリューションを記述し、小さなテストでテストし、メモ化(既に計算された値のキャッシュ)を追加し、そして---ビンゴ! ---これで完了です。
通常、再帰なしでボトムアップで機能する同等の反復プログラムを作成することもできます。この場合、これはより自然なアプローチになります:1から50までループして、フィボナッチ数をすべて計算します。
fib[0] = 0
fib[1] = 1
for i in range(48):
fib[i+2] = fib[i] + fib[i+1]
興味深いシナリオでは、ボトムアップソリューションは通常、理解するのがより困難です。ただし、一度理解すれば、通常、アルゴリズムがどのように機能するかについてより明確な全体像が得られます。実際には、自明でない問題を解決するときは、最初にトップダウンアプローチを記述し、小さな例でテストすることをお勧めします。次に、ボトムアップソリューションを記述し、2つを比較して、同じ結果が得られることを確認します。理想的には、2つのソリューションを自動的に比較します。理想的には-all特定のサイズまでの小さなテスト---の多くのテストを生成する小さなルーチンを作成し、両方のソリューションが同じ結果を与えることを検証します。その後、実稼働環境でボトムアップソリューションを使用しますが、トップボトムコードはコメントアウトします。これにより、他の開発者が自分が何をしているのかを理解しやすくなります。ボトムアップコードは、たとえそれを書いたとしても、自分が何をしているかを正確に知っていても、非常にわかりにくい場合があります。
多くのアプリケーションでは、再帰呼び出しのオーバーヘッドのため、ボトムアップのアプローチはわずかに高速です。スタックオーバーフローは特定の問題でも問題になる可能性があり、これは入力データに大きく依存する可能性があることに注意してください。場合によっては、動的プログラミングを十分に理解していないと、スタックオーバーフローを引き起こすテストを記述できない場合がありますが、いつかこれが発生する可能性があります。
現在、問題空間は非常に大きく、すべてのサブ問題を解決することができないため、トップダウンアプローチのみが実行可能なソリューションであるという問題があります。ただし、入力する必要があるのは部分的な問題のほんの一部であるため、「キャッシング」は妥当な時間で動作します。ただし、明示的に定義するのは難しいため、どの部分問題を解決する必要があり、アップソリューション。一方、allのサブ問題を解決する必要があることがわかっている場合があります。この場合、続けてボトムアップを使用します。
私は個人的にパラグラフの最適化にトップボトムを使用します- ワードラップ最適化問題 (Knuth-Plassの改行アルゴリズムを調べます;少なくともTeXはそれを使用し、Adobe Systemsの一部のソフトウェアは同様のものを使用しますアプローチ)。 高速フーリエ変換 にはボトムアップを使用します。
フィボナッチ数列を例としてみましょう
1,1,2,3,5,8,13,21....
first number: 1
Second number: 1
Third Number: 2
別の言い方をすれば、
Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21
最初の5つのフィボナッチ数の場合
Bottom(first) number :1
Top (fifth) number: 5
例として再帰的なフィボナッチ数列アルゴリズムを見てみましょう
public int rcursive(int n) {
if ((n == 1) || (n == 2)) {
return 1;
} else {
return rcursive(n - 1) + rcursive(n - 2);
}
}
次のコマンドでこのプログラムを実行すると
rcursive(5);
5番目の数値を生成するためにアルゴリズムを詳しく調べると、3番目と4番目の数値が必要です。そのため、私の再帰は実際にはtop(5)から始まり、それから下/下の数字に進みます。このアプローチは、実際にはトップダウンアプローチです。
同じ計算を複数回行わないようにするために、動的プログラミング手法を使用します。以前に計算した値を保存して再利用します。この手法はメモ化と呼ばれます。ダイナミックプログラミングには、現在の問題を議論するために必要ではないメモ化以外にもあります。
トップダウン
元のアルゴリズムを書き直し、メモ化されたテクニックを追加しましょう。
public int memoized(int n, int[] memo) {
if (n <= 2) {
return 1;
} else if (memo[n] != -1) {
return memo[n];
} else {
memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
}
return memo[n];
}
そして、次のようにこのメソッドを実行します
int n = 5;
int[] memo = new int[n + 1];
Arrays.fill(memo, -1);
memoized(n, memo);
アルゴリズムはトップ値から開始し、各ステップの一番下に移動してトップ値を取得するため、このソリューションは依然としてトップダウンです。
ボトムアップ
しかし、問題は、最初のフィボナッチ数列から始めて、上に向かって進むように、下から始められるかどうかです。このテクニックを使用して書き換えましょう。
public int dp(int n) {
int[] output = new int[n + 1];
output[1] = 1;
output[2] = 1;
for (int i = 3; i <= n; i++) {
output[i] = output[i - 1] + output[i - 2];
}
return output[n];
}
このアルゴリズムを調べてみると、実際には低い値から開始してから上に移動します。 5番目のフィボナッチ数が必要な場合、実際には1番目、2番目、3番目の5番目の数値まで計算しています。この手法は、実際にはボトムアップ手法と呼ばれています。
最後の2つは、アルゴリズムが動的プログラミングの要件を完全に満たすことです。しかし、1つはトップダウンで、もう1つはボトムアップです。両方のアルゴリズムの空間と時間の複雑さは似ています。
動的プログラミングはしばしばメモ化と呼ばれます!
1.メモ化はトップダウン手法(特定の問題を分解することで解決を開始する)であり、動的プログラミングはボトムアップ手法(些細な副問題から特定の問題に向かって解決を開始する)です
2.DPは、ベースケースから開始して解決策を見つけ、上に向かって進みます。 DPはボトムアップで行うため、すべてのサブ問題を解決します
必要な副問題のみを解決するメモ化とは異なります
DPには、指数時間ブルートフォースソリューションを多項式時間アルゴリズムに変換する可能性があります。
DPは反復的であるため、はるかに効率的です。
それどころか、Memoizationは、再帰による(多くの場合、重要な)オーバーヘッドの代価を支払わなければなりません。
より簡単にするために、メモ化はトップダウンアプローチを使用して問題を解決します。つまり、コア(メイン)問題から始まり、それをサブ問題に分割し、これらのサブ問題を同様に解決します。このアプローチでは、同じ副問題が複数回発生し、CPUサイクルをより多く消費する可能性があるため、時間の複雑さが増します。一方、動的プログラミングでは、同じ副問題が複数回解決されることはありませんが、ソリューションを最適化するために以前の結果が使用されます。
単純にトップダウンアプローチと言うと、Sub問題を何度も呼び出すために再帰を使用します
ボトムアップアプローチとしては、1つも呼び出さずにシングルを使用するため、より効率的です。
以下は、トップダウンの距離の編集問題に対するDPベースのソリューションです。ダイナミックプログラミングの世界を理解するのにも役立つことを願っています。
public int minDistance(String Word1, String Word2) {//Standard dynamic programming puzzle.
int m = Word2.length();
int n = Word1.length();
if(m == 0) // Cannot miss the corner cases !
return n;
if(n == 0)
return m;
int[][] DP = new int[n + 1][m + 1];
for(int j =1 ; j <= m; j++) {
DP[0][j] = j;
}
for(int i =1 ; i <= n; i++) {
DP[i][0] = i;
}
for(int i =1 ; i <= n; i++) {
for(int j =1 ; j <= m; j++) {
if(Word1.charAt(i - 1) == Word2.charAt(j - 1))
DP[i][j] = DP[i-1][j-1];
else
DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
}
}
return DP[n][m];
}
自宅での再帰的な実装を考えることができます。このようなものを以前に解決したことがない場合、それは非常に良いと挑戦です。