回答:
動的プログラミングとは、過去の知識を使用して将来の問題を簡単に解決できるようにすることです。
良い例は、n = 1,000,002のフィボナッチ数列を解くことです。
これは非常に長いプロセスになりますが、n = 1,000,000およびn = 1,000,001の結果を提供するとどうなりますか?突然、問題はより扱いやすくなりました。
動的プログラミングは、文字列編集の問題など、文字列の問題でよく使用されます。問題のサブセットを解決し、その情報を使用して、より困難な元の問題を解決します。
動的プログラミングでは、一般的に結果をある種のテーブルに格納します。問題の答えが必要な場合は、表を参照して、それが何であるかをすでに知っているかどうかを確認します。そうでない場合は、テーブル内のデータを使用して、答えへの足掛かりを自分に与えます。
コーメンアルゴリズムの本には、動的プログラミングに関する素晴らしい章があります。また、Googleブックスでは無料です。こちらでチェックしてください。
動的計画法は、再帰アルゴリズムで同じ部分問題を何度も計算することを避けるために使用される技法です。
フィボナッチ数列の簡単な例を見てみましょう:n 番目を見つける定義されるフィボナッチ数列を
F n = F n-1 + F n-2およびF 0 = 0、F 1 = 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
if n in cache
ますか?
メモ化は、関数呼び出しの以前の結果を保存するときです(実際の関数は、同じ入力を与えられると、常に同じものを返します)。結果が保存される前のアルゴリズムの複雑さには影響しません。
再帰は、それ自体を呼び出す関数のメソッドであり、通常はデータセットが小さくなります。ほとんどの再帰関数は同様の反復関数に変換できるため、アルゴリズムの複雑さにも影響しません。
動的プログラミングは、解決が容易な副問題を解決し、そこから答えを構築するプロセスです。ほとんどのDPアルゴリズムは、貪欲アルゴリズム(存在する場合)と指数関数(すべての可能性を列挙して最適なアルゴリズムを見つける)アルゴリズムの間の実行時間内にあります。
実行時間を短縮するのは、アルゴリズムの最適化です。
貪欲アルゴリズムは通常同じデータセットで複数回実行される可能性があるため、単純アルゴリズムと呼ばれますが、ダイナミックプログラミングでは、最終的なソリューションを構築するために保存する必要がある部分的な結果をより深く理解することにより、この落とし穴を回避します。
簡単な例は、ソリューションで寄与するノードのみを介してツリーまたはグラフをトラバースするか、同じノードを何度もトラバースしないように、これまでに見つけたソリューションをテーブルに置くことです。
UVAのオンラインジャッジ、Edit Steps Ladderによる、動的プログラミングに適した問題の例を以下に示します。
この問題の分析の重要な部分について簡単に説明します。これは、 『プログラミングの挑戦』の本から引用したものですので、ぜひご覧ください。
その問題をよく見てください。2つの文字列がどれだけ離れているかを示すコスト関数を定義する場合、3つの自然なタイプの変更を2つ検討します。
置換-「ショット」を「スポット」に変更するなど、単一の文字をパターン「s」からテキスト「t」の別の文字に変更します。
挿入-パターン「s」に単一の文字を挿入して、「ago」を「agog」に変更するなど、テキスト「t」に一致させるのに役立ちます。
削除-パターン「s」から1文字を削除して、「hour」を「our」に変更するなど、テキスト「t」との一致を支援します。
この各操作を1ステップのコストに設定すると、2つの文字列間の編集距離が定義されます。では、それをどのように計算するのでしょうか?
文字列の最後の文字を一致、置換、挿入、または削除する必要があるという観察を使用して、再帰アルゴリズムを定義できます。最後の編集操作で文字を切り落とすと、ペア操作が残り、ペアの小さな文字列が残ります。iとjを、それぞれ関連するプレフィックスの最後の文字とします。最後の操作の後に、一致/置換、挿入、または削除後の文字列に対応する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の割合で成長します。実際、ほとんどの呼び出しは2つのインデックスの両方ではなく、1つだけを減らすため、さらに高速です。
それでは、アルゴリズムを実用的にするにはどうすればよいでしょうか。重要な観察は、これらの再帰呼び出しのほとんどが、以前にすでに計算されたものを計算しているということです。どうやって知るの?まあ、| 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つの違いがあります。
まず、再帰呼び出しの代わりにテーブル検索を使用して中間値を取得します。
** 2番目に、**各セルの親フィールドを更新します。これにより、後で編集シーケンスを再構築できます。
** 3番目、** 3番目、
cell()
m [| s |] [| t |] .costを返すだけでなく、より一般的なゴール関数を使用して計測されます。これにより、このルーチンをより幅広いクラスの問題に適用できるようになります。
ここでは、最適な部分的な結果を収集するために必要なことの非常に特定の分析が、ソリューションを「動的」なものにします。
これは、同じ問題に対する代替の完全な解決策です。実行方法は異なりますが、「動的」なものでもあります。UVAのオンラインジャッジに提出することで、ソリューションの効率性を確認することをお勧めします。そのような重い問題がいかに効率的に対処されたかは驚くべきものです。
動的プログラミングの主要なビットは、「重複するサブ問題」と「最適なサブ構造」です。問題のこれらの特性は、最適解がその副問題の最適解で構成されることを意味します。たとえば、最短経路の問題は最適な部分構造を示します。AからCへの最短パスは、AからノードBへの最短パスであり、その後にそのノードBからCへの最短パスが続きます。
より詳細には、最短経路問題を解決するには、次のようにします。
私たちはボトムアップで作業しているため、サブ問題をメモすることで、それらを使用するときの解決策をすでに持っています。
動的プログラミングの問題には、重複するサブ問題と最適なサブ構造の両方が必要であることに注意してください。フィボナッチ数列の生成は動的プログラミングの問題ではありません。重複するサブ問題があるためメモ化を利用しますが、最適化のサブ構造はありません(最適化の問題がないため)。
動的プログラミング
定義
動的計画法(DP)は、副問題が重複する問題を解決するための一般的なアルゴリズム設計手法です。この手法は、1950年代にアメリカの数学者「リチャードベルマン」によって発明されました。
重要なアイデア
重要なアイデアは、重複する小さなサブ問題の回答を保存して、再計算を回避することです。
動的プログラミングのプロパティ
また、私はダイナミックプログラミング(特定の種類の問題のための強力なアルゴリズム)を非常に初めて使用しています。
最も単純な言葉で言えば、動的プログラミングは以前の知識を使用した再帰的なアプローチと考えてください。
以前の知識は、ここで最も重要なものです。すでに持っている副問題の解決策を追跡してください。
ウィキペディアの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またはサブ問題のより多くの値が再計算され、指数時間アルゴリズムが発生します。
今、言って私たちはすでにデータ構造内で見つかった値を格納することで、それを試すことができます地図
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つのステップは次のとおりです。
6. Convert the memoized recursive algorithm into iterative algorithm
必須のステップは?これは、その最終的な形式が非再帰的であることを意味しますか?
つまり、再帰メモ化と動的プログラミングの違い
名前が示唆する動的プログラミングは、前の計算値を使用して次の新しいソリューションを動的に構築します
動的プログラミングを適用する場所:ソリューションが最適な部分構造と重なり合う部分問題に基づいている場合、その場合は以前に計算された値を使用すると便利なので、再計算する必要はありません。ボトムアップアプローチです。その場合、fib(n)を計算する必要があるとしましょう。必要なのは、前に計算されたfib(n-1)とfib(n-2)の値を加算することだけです。
再帰:基本的に、問題を小さな部分に細分して簡単に解決しますが、以前に他の再帰呼び出しで同じ値を計算した場合、再計算は避けられないことに注意してください。
メモ化:基本的に、古い計算された再帰値をテーブルに格納することはメモ化と呼ばれ、以前の呼び出しによって既に計算されている場合は再計算を回避するため、値は1回だけ計算されます。したがって、計算する前に、この値がすでに計算されているかどうかを確認します。計算済みの場合は、再計算するのではなく、テーブルから同じ値を返します。トップダウンのアプローチでもあります
ここでの簡単なPythonのコード例でありRecursive
、Top-down
、Bottom-up
フィボナッチ数列のためのアプローチは:
def fib_recursive(n):
if n == 1 or n == 2:
return 1
else:
return fib_recursive(n-1) + fib_recursive(n-2)
print(fib_recursive(40))
def fib_memoize_or_top_down(n, mem):
if mem[n] is not 0:
return mem[n]
else:
mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
return mem[n]
n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))
def fib_bottom_up(n):
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
if n == 1 or n == 2:
return 1
for i in range(3, n+1):
mem[i] = mem[i-1] + mem[i-2]
return mem[n]
print(fib_bottom_up(40))