動的プログラミングとは、過去の知識を使用して将来の問題を簡単に解決することです。
良い例は、n = 1,000,002のフィボナッチ数列を解くことです。
これは非常に長いプロセスになりますが、n = 1,000,000およびn = 1,000,001の結果を提供するとどうなりますか?突然、問題はより管理しやすくなりました。
動的プログラミングは、文字列編集の問題など、文字列の問題で多く使用されます。問題のサブセットを解決し、その情報を使用して、より困難な元の問題を解決します。
動的プログラミングでは、一般的に何らかの結果をテーブルに保存します。問題に対する答えが必要なときは、表を参照して、それが何であるかをすでに知っているかどうかを確認します。そうでない場合は、テーブル内のデータを使用して、答えに向けた足掛かりを自分自身に与えます。
Cormen Algorithmsブックには、動的プログラミングに関するすばらしい章があります。また、Googleブックスでは無料です!確認してください こちら
動的プログラミングは、再帰アルゴリズムで同じ副問題を何度も計算することを避けるために使用される手法です。
フィボナッチ数の簡単な例を見てみましょう:nを見つける 番目 フィボナッチ数
Fn = Fn-1 + Fn-2 とF = 0、F1 = 1
これを行う明白な方法は再帰的です:
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
特定のフィボナッチ数が複数回計算されるため、再帰では多くの不要な計算が行われます。これを改善する簡単な方法は、結果をキャッシュすることです:
cache = {}
def fibonacci(n):
if n == 0:
return 0
if n == 1:
return 1
if n in cache:
return cache[n]
cache[n] = fibonacci(n - 1) + fibonacci(n - 2)
return cache[n]
これを行うためのより良い方法は、正しい順序で結果を評価することにより、再帰をすべて取り除くことです。
cache = {}
def fibonacci(n):
cache[0] = 0
cache[1] = 1
for i in range(2, n + 1):
cache[i] = cache[i - 1] + cache[i - 2]
return cache[n]
一定のスペースを使用して、途中で必要な部分的な結果のみを保存することもできます。
def fibonacci(n):
fi_minus_2 = 0
fi_minus_1 = 1
for i in range(2, n + 1):
fi = fi_minus_1 + fi_minus_2
fi_minus_1, fi_minus_2 = fi, fi_minus_1
return fi
動的プログラミングをどのように適用しますか?
通常、動的プログラミングは、文字列、ツリー、整数シーケンスなど、固有の左から右の順序を持つ問題に対して機能します。単純な再帰アルゴリズムが同じ副問題を複数回計算しない場合、動的プログラミングは役に立ちません。
ロジックを理解するのに役立つ問題のコレクションを作成しました: https://github.com/tristanguigue/dynamic-programing
同様のトピックで my answe r
皮切りに
あなた自身をテストしたい場合、オンライン裁判官についての私の選択は
そしてもちろん
良い大学のアルゴリズムコースもチェックできます。
結局、問題を解決できない場合は、SOに質問してください。
メモ化は、関数呼び出しの以前の結果を保存するときです(同じ入力が与えられると、実際の関数は常に同じものを返します)。結果が保存される前に、アルゴリズムの複雑さに違いは生じません。
再帰は、それ自体を呼び出す関数のメソッドであり、通常はより小さいデータセットを使用します。ほとんどの再帰関数は、同様の反復関数に変換できるため、アルゴリズムの複雑さにも違いはありません。
動的プログラミングは、解決しやすいサブ問題を解決し、そこから答えを構築するプロセスです。ほとんどのDPアルゴリズムは、貪欲アルゴリズム(存在する場合)と指数アルゴリズム(すべての可能性を列挙し、最適なアルゴリズムを見つける)の間の実行時間内にあります。
実行時間を短縮するのは、アルゴリズムの最適化です。
貪欲なアルゴリズムは通常naiveと呼ばれますが、同じデータセットに対して複数回実行される可能性があるため、ダイナミックプログラミングでは、最終的なソリューションの構築を支援するために保存されます。
簡単な例は、ソリューションに貢献するノードのみをツリーまたはグラフでトラバースするか、同じノードを何度もトラバースしないように、これまでに見つけたソリューションをテーブルに入れます。
UVAのオンライン裁判官による、動的プログラミングに適した問題の例を次に示します。 Edit Steps Ladder。
「プログラミングの課題」という本から抜粋した、この問題の分析の重要な部分について簡単に説明します。ぜひチェックしてみてください。
その問題をよく見てください。2つの文字列がどのくらい離れているかを示すコスト関数を定義する場合、2つの3つの自然な変化を考慮する必要があります。
置換-「ショット」を「スポット」に変更するなど、単一の文字をパターン「s」からテキスト「t」の別の文字に変更します。
挿入-「ago」を「agog」に変更するなど、テキスト「t」と一致させるために、パターン「s」に単一の文字を挿入します。
削除-パターン「s」から単一の文字を削除して、「hour」を「our」に変更するなど、テキスト「t」と一致するようにします。
この各操作に1ステップかかるように設定すると、2つの文字列間の編集距離が定義されます。それでは、どのように計算するのでしょうか?
文字列の最後の文字を一致、置換、挿入、または削除する必要があるという観察を使用して、再帰アルゴリズムを定義できます。最後の編集操作で文字を切り落とすと、ペア操作が残り、小さな文字列のペアが残ります。 iとjを、それぞれ関連する接頭辞とtの最後の文字とします。最後の操作の後に、一致/置換、挿入、または削除後の文字列に対応する3つの短い文字列のペアがあります。 3組の小さな文字列を編集するコストを知っていれば、どのオプションが最適なソリューションにつながるかを判断し、それに応じてそのオプションを選択できます。再帰というすばらしいことを通して、このコストを学ぶことができます。
#define MATCH 0 /* enumerated type symbol for match */
> #define INSERT 1 /* enumerated type symbol for insert */
> #define DELETE 2 /* enumerated type symbol for delete */
>
>
> int string_compare(char *s, char *t, int i, int j)
>
> {
>
> int k; /* counter */
> int opt[3]; /* cost of the three options */
> int lowest_cost; /* lowest cost */
> if (i == 0) return(j * indel(’ ’));
> if (j == 0) return(i * indel(’ ’));
> opt[MATCH] = string_compare(s,t,i-1,j-1) +
> match(s[i],t[j]);
> opt[INSERT] = string_compare(s,t,i,j-1) +
> indel(t[j]);
> opt[DELETE] = string_compare(s,t,i-1,j) +
> indel(s[i]);
> lowest_cost = opt[MATCH];
> for (k=INSERT; k<=DELETE; k++)
> if (opt[k] < lowest_cost) lowest_cost = opt[k];
> return( lowest_cost );
>
> }
このアルゴリズムは正しいですが、非常に遅いです。
コンピューター上で実行すると、2つの11文字の文字列を比較するのに数秒かかり、計算は消えて決して長くはならないことになります。
なぜアルゴリズムがそんなに遅いのですか?値を何度も何度も再計算するため、指数関数的な時間がかかります。文字列内のすべての位置で、再帰は3つの方法で分岐します。つまり、少なくとも3 ^ nの速度で増加します。
それでは、どのようにアルゴリズムを実用的にすることができますか? 重要な所見は、これらの再帰呼び出しのほとんどが以前に計算されたものを計算していることです。どうやって知るのですか?まあ、| s |しかありません・| t |再帰呼び出しのパラメーターとして機能する個別の(i、j)ペアが多数あるため、可能な一意の再帰呼び出し。
これらの(i、j)ペアのそれぞれの値をテーブルに保存することにより、それらを再計算することを避け、必要に応じてそれらを検索することができます。
テーブルは2次元行列mであり、| s |・| t |のそれぞれがセルには、このサブ問題の最適なソリューションのコストと、この場所に到達した方法を説明する親ポインターが含まれています。
typedef struct { int cost; /* cost of reaching this cell */ int parent; /* parent cell */ } cell; cell m[MAXLEN+1][MAXLEN+1]; /* dynamic programming table */
動的プログラミングバージョンには、再帰バージョンとの3つの違いがあります。
最初に、再帰呼び出しの代わりにテーブル検索を使用して中間値を取得します。
**第二に、**各セルの親フィールドを更新します。これにより、後で編集シーケンスを再構築できます。
**第三、**第三に、m [| s |] [| t |] .costを返す代わりに、より一般的な目標cell()関数を使用してインストルメント化されます。これにより、このルーチンをより広範なクラスの問題に適用できます。
ここで、最適な部分結果を収集するために必要なものの非常に特別な分析は、ソリューションを「動的」なものにするものです。
こちら 同じ問題に対する代替の完全なソリューション。実行方法は異なりますが、「動的」なものでもあります。 UVAのオンライン裁判官に提出することで、ソリューションの効率を確認することをお勧めします。このような重大な問題がいかに効率的に対処されたかに驚く。
動的プログラミングの重要な部分は、「重複する部分問題」と「最適な部分構造」です。問題のこれらの特性は、最適なソリューションがそのサブ問題の最適なソリューションで構成されることを意味します。たとえば、最短経路の問題は最適な部分構造を示します。 AからCへの最短経路は、AからノードBへの最短経路と、それに続くノードBからCへの最短経路です。
より詳細には、最短経路の問題を解決するには、次のことを行います。
ボトムアップで作業しているため、サブ問題をメモすることで、サブ問題を使用するときの解決策をすでに持っています。
動的プログラミングの問題には、部分的な問題と最適な部分構造が重複している必要があることを忘れないでください。フィボナッチ数列の生成は、動的プログラミングの問題ではありません。重複する副問題があるため、メモ化を利用しますが、最適化の下位構造はありません(最適化の問題がないため)。
CMUのMichael A. Trickによるチュートリアルが特に役立ちました。
http://mat.gsia.cmu.edu/classes/dynamic/dynamic.html
確かに、他の人が推奨しているすべてのリソースに加えて(他のすべてのリソース、特にCLRとKleinberg、Tardosは非常に優れています!)。
このチュートリアルが好きな理由は、高度な概念をかなり徐々に紹介するためです。少し古めかしい資料ですが、ここで紹介するリソースのリストに追加するのに適しています。
また、Steven Skienaのページとダイナミックプログラミングに関する講義もご覧ください。 http://www.cs.sunysb.edu/~algorith/video-lectures/
http://www.cs.sunysb.edu/~algorith/video-lectures/1997/lecture12.pdf
動的プログラミング
定義
動的計画法(DP)は、重複する副問題の問題を解決するための一般的なアルゴリズム設計手法です。この技術は、1950年代にアメリカの数学者「リチャードベルマン」によって発明されました。
重要なアイデア
重要なアイデアは、重複する小さなサブ問題の回答を保存して、再計算を回避することです。
動的プログラミングプロパティ
私はまた、ダイナミックプログラミング(特定の種類の問題に対する強力なアルゴリズム)が初めてです。
最も単純な言葉で言えば、動的プログラミングは、以前の知識を使用した再帰的アプローチと考えてください
以前の知識はここで最も重要なことです、あなたがすでに持っている副問題の解決策を追跡してください。
Wikipediaのdpの最も基本的な例を考えてみましょう
フィボナッチ数列を見つける
function fib(n) // naive implementation
if n <=1 return n
return fib(n − 1) + fib(n − 2)
たとえばn = 5で関数呼び出しを分解しましょう
fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
特に、fib(2)はゼロから3回計算されました。大きな例では、fibのより多くの値、または副問題が再計算され、指数時間アルゴリズムになります。
さて、すでに見つけた値を Map などのデータ構造に保存して試してみましょう
var m := map(0 → 0, 1 → 1)
function fib(n)
if key n is not in map m
m[n] := fib(n − 1) + fib(n − 2)
return m[n]
ここで、サブ問題の解決策をマップに保存します(まだ解決していない場合)。すでに計算した値を保存するこの手法は、メモ化と呼ばれます。
最後に、問題については、最初に状態を見つけてみてください(可能性のあるサブ問題を解決し、以前のサブ問題の解決策をさらに使用できるように、より良い再帰アプローチを考えてみてください)。
動的計画法は、サブ問題が重複する問題を解決するための手法です。動的プログラミングアルゴリズムは、すべてのサブ問題を一度だけ解決し、その答えをテーブル(配列)に保存します。サブ問題が発生するたびに答えを再計算する作業を回避します。動的プログラミングの基本的な考え方は次のとおりです。通常、サブ問題の既知の結果の表を保持することにより、同じものを2回計算することを避けます。
ダイナミックプログラミングアルゴリズムの開発における7つのステップは次のとおりです。
要するに、再帰メモ化と動的プログラミングの違い
名前が示すとおり、動的プログラミングでは、前の計算値を使用して、次の新しいソリューションを動的に構築します
動的計画法を適用する場所:解決策が最適な部分構造と重複する部分問題に基づいている場合、その場合、以前に計算された値を使用すると役立つため、再計算する必要はありません。それはボトムアップのアプローチです。その場合、fib(n-1)とfib(n-2)の以前の計算値を追加するだけで、fib(n)を計算する必要があるとします
再帰:基本的に問題をより小さな部分に細分して簡単に解決しますが、他の再帰呼び出しで以前に同じ値が計算された場合、再計算を回避しないことに注意してください。
メモ化:基本的に、古い計算された再帰値をテーブルに保存することはメモ化と呼ばれ、以前の呼び出しによって既に計算されている場合は再計算を回避します。したがって、計算する前に、この値がすでに計算されているかどうかを確認します。計算済みの場合は、再計算する代わりにテーブルから同じ値を返します。トップダウンアプローチでもあります